Docker Compose: auto-start on boot
Make Docker Compose services come back after a reboot: restart policies, why on-failure does not survive one, and when a systemd unit is right.
The short answer
Docker Compose services start on boot when two conditions hold at the same time. The Docker daemon has to be enabled as a system service, and each service in the file has to carry a restart policy of unless-stopped or always. Add restart: unless-stopped to every service, run docker compose up -d once, and the containers come back on their own after a reboot. Nothing else is required for the common case.
You need a systemd unit only when the order matters: a stack that depends on a mounted disk, a VPN interface, or a network share that is not ready at the moment the Docker daemon starts. That case is real, and the second half of this guide covers it. If you are still finding your way around service definitions and volumes, start with the basics of Docker Compose on a VPS and come back.
Set the restart policy in compose.yaml
The policy is one line per service. There is no global switch, so a service you forget stays down after the reboot while the rest of the stack comes up.
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:Apply it and then read the policy back off the running container:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)That prints unless-stopped. If it prints no, the file was edited but the container was never recreated.
This is the single most common failure. The restart policy is stored on the container, not in the YAML file. Editing compose.yaml changes nothing about a container that already exists. docker compose restart does not help either, because it stops and starts the same container object without touching its configuration. Only docker compose up -d compares the file to the running containers, notices the policy changed, and recreates them.
For a container you do not want to recreate right now, change the policy in place:
docker update --restart unless-stopped my-containerStill edit the YAML file as well. docker update changes the live container, and the next docker compose up -d will read the file and put the old value back.
What each restart value actually does
Docker defines four values, and the difference between them only shows up when the machine reboots or the daemon is restarted.
nois the default. The container is never restarted automatically, under any circumstance.alwaysrestarts the container whenever it stops. If you stopped it by hand, it comes back anyway the next time the Docker daemon starts. That is often surprising: a container you deliberately stopped last week is running again after a reboot.unless-stoppedbehaves likealways, except that a container stopped by hand stays stopped across a daemon restart. This is the value you want for a service you occasionally take down for maintenance.on-failurerestarts the container only when it exits with a non-zero exit code. You can cap the attempts, as inrestart: on-failure:3.
For a stack that should simply be up whenever the server is up, unless-stopped is the right default. Choose always only when you want a container that resists being left down.
Why restart: on-failure does not survive a reboot
Many people pick on-failure because it sounds careful, then find every container stopped after the first reboot. The reason is in the definition. on-failure reacts to one thing only: the container process exiting with an error code.
A reboot is not an error. When the host shuts down, systemd stops docker.service, and the daemon stops each container deliberately. The container did not fail, so the policy has nothing to react to. On the way back up, the daemon looks at containers it needs to resume, and an on-failure container that was cleanly stopped is not one of them. It stays in the exited state.
You can see it directly. Set restart: on-failure on a service, run docker compose up -d, reboot, then run:
docker compose ps -aThe service is listed with a state of Exited and a status like Exited (0) 2 minutes ago. Nothing is broken and nothing is logged as an error, which is what makes this hard to diagnose. The policy did exactly what it says.
on-failure is still useful. It fits a container that runs a job and may crash, where you want a bounded number of retries and no restart loop. It is the wrong tool for keeping a long-running service alive across reboots.
Restart policies only work if the Docker service starts at boot
Restart policies are enforced by the Docker daemon. If the daemon does not start, nothing enforces anything. Check it:
systemctl is-enabled docker
systemctl is-enabled containerdBoth should print enabled. The packages from Docker's official repository enable them at install time, so on a fresh server this usually passes. If either prints disabled, fix it:
sudo systemctl enable --now docker containerdThere is a trap here worth understanding. Ubuntu also ships docker.socket, which starts the daemon on demand the first time something talks to the Docker API. People see docker.socket enabled, assume the daemon is covered, and disable docker.service to save memory. At boot, nothing calls the API, so the socket is never touched, the daemon never starts, and no container comes up until you type your first docker command. Socket activation is not a substitute for docker.service being enabled.
When a systemd unit is the better answer
Restart policies have no concept of ordering against the rest of the system. The daemon starts, and it brings your containers up as soon as it can. If your stack bind-mounts a directory from a separate volume, an NFS (network file system) share, or an encrypted disk, the containers may start before that path exists. Docker will happily create an empty directory at the mount point and start the container against it, and your database comes up with no data.
Write a systemd unit when any of these apply. The stack needs a mount, a VPN interface, or another unit to be ready first. You want systemctl stop myapp and systemctl start myapp to work like they do for every other service on the box. Or you want the stack brought down cleanly during shutdown instead of being killed alongside the daemon. If systemd units are new to you, writing a systemd service and timer covers the file format in more detail.
Writing the systemd unit
Put the stack in a fixed path outside a home directory. /srv/myapp is a good choice, because a unit that runs before anyone logs in has no business reading /home.
Create /etc/systemd/system/myapp.service:
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.targetEnable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.serviceA healthy unit shows Active: active (exited). That looks wrong the first time you see it. It is correct: Type=oneshot with RemainAfterExit=yes means the unit ran its command, the command finished, and systemd keeps the unit marked active so ExecStop runs at shutdown.
Each line earns its place. Requires=docker.service means the unit fails fast rather than running docker compose against a dead socket. After= sets the order, because Requires= on its own does not. RequiresMountsFor= makes systemd pull in the mount unit for that path and wait for it, which is the whole reason to use a unit instead of a restart policy. TimeoutStartSec=0 stops systemd from killing the start job while a large image is still being pulled.
A note on combining the two mechanisms. Docker's documentation advises against mixing restart policies with a host process manager. That warning is about a process manager that supervises the container process itself and restarts it while the daemon is trying to do the same. A Type=oneshot unit supervises nothing, so keeping restart: unless-stopped in the compose file alongside this unit is fine, and it is what you want. systemd handles the ordering at boot, and the daemon handles a container that crashes at three in the morning.
Verify with a real reboot
There is no substitute for the real test. systemctl restart docker does not exercise mount ordering, and docker compose down followed by docker compose up -d does not exercise anything about boot at all.
sudo rebootWait, reconnect, and check in this order:
uptime
systemctl is-active docker
docker compose psuptime confirms you are looking at a machine that really rebooted. docker compose ps, run from the stack directory, should list every service as running with an uptime close to the machine's. A service showing Exited is the one to look at.
If something did not come up, the daemon log covers the boot window:
journalctl -u docker.service -b --no-pager | tail -50For a stack managed by a unit, journalctl -u myapp.service -b --no-pager shows the exact docker compose output from boot, including a failed image pull or a missing .env file.
Things that quietly break auto-start
Containers created with docker compose run never get the restart policy from the file. Compose treats them as one-off containers. If a service seems to ignore its policy, check whether it was started with run instead of up.
A relative path in a volume or in an env_file entry is resolved against the compose file's directory. That works from your shell, and it works from a unit that sets WorkingDirectory. It fails from a unit without one, because the working directory is then /.
Rootless Docker is a separate case. The daemon runs as a user service, and a user service stops when the last session for that user ends. Enable it for the user and allow it to keep running with nobody logged in:
systemctl --user enable docker
sudo loginctl enable-linger $USERWithout enable-linger, the rootless daemon shuts down when you log out and the containers go with it, which looks exactly like a broken restart policy.
One last thing. Automatic security updates can reboot a server at a fixed hour, which is only a good thing if your stack comes back by itself. Setting that up on a new machine belongs with the rest of the first-hour work in the first ten minutes on a new VPS.
FAQ
What is the difference between restart: always and restart: unless-stopped?
Both restart the container when it stops on its own. They differ after you stop a container by hand. With always, the container starts again the next time the Docker daemon starts, so a reboot undoes your manual stop. With unless-stopped, the daemon remembers that the container was stopped deliberately and leaves it alone. Use unless-stopped unless you specifically want a container that will not stay down.
I added restart: unless-stopped but the container still does not start after a reboot. Why?
The policy lives on the container, not in the file, and a container that already exists is not updated by editing YAML. Run docker compose up -d so Compose recreates it, then confirm with docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app). If that prints no, the container predates your edit. The other common cause is docker.service not being enabled, which you can check with systemctl is-enabled docker.
Do I need a systemd unit if I already use restart policies?
Usually not. A restart policy is enough for a stack that only needs the network, which is most stacks. Add a unit when the containers depend on something that is not ready when the Docker daemon starts, such as an external disk, an encrypted volume, an NFS share, or a VPN interface. The unit gives you ordering through After= and RequiresMountsFor=, which a restart policy cannot express.
How do I stop a stack permanently without it coming back on the next reboot?
With unless-stopped, docker compose stop is enough, because a container stopped by hand is not resumed when the daemon restarts. With always, a stop is not enough and the container returns after a reboot. Either run docker compose down, which removes the containers, or change the policy first with docker update --restart no my-container. If a systemd unit manages the stack, run sudo systemctl disable myapp.service as well, or the unit will start it again.