When I get a virgin server (fresh Ubuntu, nothing installed yet), I do not grab Docker from a random
apt install docker.io and hope for the best. I want the official packages, Compose as a plugin,
a normal user who can run containers without typing sudo every time, and a layout I can maintain months later.
This is the checklist I use. Examples below assume Ubuntu 22.04 or 24.04. The same idea works on Debian with small path changes.
sudo, a public IP or hostname,
and a plan for which ports you will open (usually 22, 80, and 443).
1. Update the system first
Always patch before you install anything important.
sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
2. Remove old Docker leftovers (if any)
On a true virgin server this may do nothing. On a reused box it stops odd conflicts.
sudo apt remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true
3. Add Docker’s official repository
Do not rely on Ubuntu’s default Docker package for production work. Use Docker’s own apt repo.
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
4. Install Engine, CLI, containerd, and Compose
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Enable and start the service:
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
You should see active (running).
5. Quick proof that it works
sudo docker run --rm hello-world
If that prints the hello message and exits cleanly, the daemon is fine. Check versions too:
docker --version
docker compose version
Note the space in docker compose. That is the plugin. The old standalone
docker-compose binary is not what we install here.
6. Let your user run Docker without sudo
Running everything as root is messy. Add your login user to the docker group.
Replace mark with your username.
sudo usermod -aG docker mark
# log out and SSH back in, then:
groups
docker run --rm hello-world
docker group can effectively get root on the host through the Docker socket.
Only add people you trust on that machine.
7. Firewall basics (UFW)
Docker can publish ports even when UFW looks closed, so be deliberate about what you expose. Still, start with a sane host firewall:
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Only open app ports you actually need. Prefer putting public traffic behind a reverse proxy (Traefik or nginx) instead of publishing every container port to the world.
8. A simple folder layout I like
Keep projects under one place so backups and deploys stay predictable.
sudo mkdir -p /opt/apps
sudo chown mark:mark /opt/apps
mkdir -p /opt/apps/demo
cd /opt/apps/demo
Example docker-compose.yml for a tiny web service:
services:
web:
image: nginx:alpine
ports:
- "8080:80"
restart: unless-stopped
Bring it up:
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8080
Stop and remove when you are done testing:
docker compose down
9. Habits that keep a “proper” setup healthy
- Pin images when it matters. Prefer tags like
nginx:1.27-alpineover floatinglatestin production. - Use named volumes for data. Databases and uploads should not live only inside a writable container layer.
- Put secrets in env files or a secret store. Do not commit
.envwith passwords. Keep a.env.exampleinstead. - Log rotation. Default Docker logs can fill a disk. Configure json-file rotation or ship logs elsewhere.
- Update on purpose. Patch the host regularly, then update images with a plan and a rollback.
Example daemon log rotation in /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Then restart Docker:
sudo systemctl restart docker
10. Optional: auto-start compose stacks on reboot
With restart: unless-stopped (as in the example above), containers come back after a reboot
as long as the Docker service starts. That is usually enough.
For more control, keep a small systemd unit that runs docker compose up -d in the project folder
after Docker is ready. I only add that when a stack needs an ordered bring-up.
What I avoid on a fresh box
- Installing Docker only from Ubuntu’s default
docker.iopackage for long-lived servers. - Exposing the Docker socket to random containers “for convenience.”
- Publishing database ports (5432, 3306) to
0.0.0.0on the public internet. - Running production apps as root inside the container when the image supports a non-root user.
Short “done” checklist
dockeranddocker composework for your user- Docker service enabled on boot
- SSH and only needed web ports allowed
- Apps live under something like
/opt/apps/... - Log rotation set, secrets kept out of git
That is enough for a clean start. From there you can add Traefik or nginx-proxy, TLS certificates, and your real application stacks without fighting a messy first install.