Moving my side projects from Compose to systemd units
For years every side project I ran lived in its own docker-compose.yml. It worked, but a small VPS was spending more memory on the container runtime than on my actual applications. Most of these projects are a single Go or Python binary plus SQLite, so this spring I tried running them as plain systemd services.
A unit file is shorter than you think
[Unit]
Description=Bookmarks service
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/opt/bookmarks/bookmarks --listen 127.0.0.1:8081
WorkingDirectory=/var/lib/bookmarks
DynamicUser=yes
StateDirectory=bookmarks
Restart=on-failure
[Install]
WantedBy=multi-user.target
DynamicUser creates a throwaway user at start, and StateDirectory gives it a persistent, correctly owned directory under /var/lib. That covers most of what I used containers for.
Sandboxing for free
systemd has a surprisingly complete set of isolation options. These lines make the service read-only except for its state directory and hide other users' processes:
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
ProtectProc=invisible
Run systemd-analyze security bookmarks.service to get a score and a list of further suggestions.
Logs in one place
Everything goes to journald, so journalctl -u bookmarks -f replaces docker compose logs, and log rotation is handled for me. Boot time on the VPS dropped from about 40 to 12 seconds.
What I would do differently
- Keep Compose for anything that needs PostgreSQL or Redis next to it — reproducing those setups by hand is not worth it.
- Build static binaries in CI; copying one file is the whole deployment.
- Use
systemd-run --scopefor one-off jobs instead of cron when you want resource limits.
Containers are still the right tool for many things. For a handful of tiny personal services, the init system I already had turned out to be enough.