→ From Coder to Shipper
From Coder to Shipper · DevOps

systemd — services & keeping things running

The program that boots your server, starts your services, and quietly brings them back every time they die. One mental model + four commands = daily ops.

← / → to navigate · EN/VI switch below

Big picture · first things first

The first program that runs

When a server powers on, the kernel's very first job is to start exactly one program: systemd. Everything else on the machine waits for it.

⚡ Power on

the machine boots

▶

🧠 Kernel

loads from disk, inits hardware

▶

👑 systemd

PID 1 — the first process, the boss

runs forever

▶

⚙️ Your services

nginx · database · your app — started by systemd

no one starts systemd — the kernel starts it, and it starts everyone else

Big picture · what it is

One program, three jobs

systemd is the init system — the process that owns everything after boot. It wears three hats at once.

PID 1 — the first process

The first program the kernel starts. Every other process on the machine is, in the end, its descendant.

Service manager

Reads unit files and starts or stops each service exactly according to its instructions.

The janitor

Watches every service it started — and restarts it when it crashes, whether that's 3 p.m. or 3 a.m.

Big picture · why it exists

Two problems, one answer

Before 'just run a program on a server' works, two things must be true. systemd exists to guarantee both.

It must survive a reboot

Someone reboots the machine — an update, a power blip. Your site must be back on the air without anyone logging in.

→ solved by: ENABLE

It must survive a crash

A bug, out of memory, a bad deploy — the process dies. It must come back by itself, immediately.

→ solved by: Restart=always

Your server has to work while you sleep. systemd is the colleague who never sleeps: it starts the whole team at boot and revives anyone who drops.
the whole topic in one sentence
01
Part 1

The mental model

What a 'service' really is, and why the program in your terminal is not one.

Mental model · picture it

systemd is the parent of everything

Every process on Linux has a parent. Follow the parents up and you always end at systemd — the root of the process tree.

👑 systemd — PID 1

the supervisor · runs for the whole life of the machine

▼ ▼ ▼

🌐 nginx

serves the web

🐍 app.service

your Python app

🗄 postgresql

the database

kill a service and its processes die with it — then systemd can bring it right back

Mental model · the core idea

A service = a plain-text instruction sheet

A 'service' is nothing magical: it's a small file (a unit) that tells systemd how to run one program and keep it alive.

app.service

The file's name is the service name. systemd reads it and gets the whole job description.

What to run

The exact command, the user it runs as, the working directory, the environment variables.

When to run

Whether it starts automatically at boot (WantedBy=multi-user.target).

What happens when it dies

The restart policy: come back always, only on failure, or stay down.

Mental model · the boot sequence

From power button to your site

  1. 1

    Kernel starts systemd

    systemd becomes PID 1 — the first process, the root of everything on the machine.

  2. 2

    systemd reads the unit files

    It scans /etc/systemd/system/ and /lib/systemd/system/ for .service files.

  3. 3

    Enabled services start

    Every unit whose [Install] section says 'start me at boot' comes up, in the right order.

  4. 4

    Supervision begins

    systemd watches each service for the rest of the machine's life — restarting any that crash.

Mental model · the difference

Your terminal program is not a service

python server.py in your terminal and a systemd service can run the exact same code — with very different fates.

Terminal program

Tied to your session: close the terminal or the SSH drops, and it dies. It crashes? Dead until someone re-runs it by hand.

Great for development — fatal on a server.

systemd service

Detached from any terminal. Survives you logging out and the machine rebooting — and restarts itself when it crashes.

This is what 'running in production' actually means.

Mental model · the scope

Everything on a server is a service

You don't 'install and run' software on a Linux box — you add services. The whole machine is a team of services.

nginx

The web server — a service that serves your site.

postgresql

The database — a service that keeps the data.

app.service

Your Python/Flask app — the service you write the unit file for.

sshd

SSH — the very door you use to administer the server.

cron

Scheduled jobs — the 3 a.m. backups.

docker

The container runtime itself — also just another service.

02
Part 2

systemctl — the remote control

One command talks to systemd. Four verbs cover 90% of what you'll ever type.

systemctl · the verbs

Four verbs

All typed the same way: sudo systemctl <verb> <service>.

start

Start the service now — for this boot only. The next reboot won't remember, unless you enable it.

stop

Stop it now. It will still start at the next boot, unless you disable it.

restart

Stop + start, in one move. The daily go-to after a deploy or a config change.

status

Alive? Since when? What did it log recently? The first command of every single incident.

systemctl · see it

systemctl status — the health check

One command: the service's state, how long it's been up, and its most recent log lines.

ops@vps — zsh
$ sudo systemctl status app
● app.service - My Flask App
Loaded: loaded (/etc/systemd/system/app.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2026-10-26 09:12:01 UTC; 3h 14min ago
Main PID: 1042 (python3)
Memory: 48.2M
CGroup: /system.slice/app.service
└─1042 /usr/bin/python3 /opt/app/server.py
Oct 26 09:12:01 vps systemd[1]: Started app.service - My Flask App.
"active (running)" = alive · the last lines come from the journal
systemctl · the classic confusion

ENABLE vs START — the #1 confusion

Two switches, two different times: one is about 'now', the other about 'every boot'. They work independently.

START — now

sudo systemctl start app turns the process on in the current session. Reboot the machine and it's gone — unless it's also enabled.

Think: pressing the power button on an AC.

ENABLE — every boot

sudo systemctl enable app tells systemd: start this automatically at every boot. It does NOT start it right now.

Think: plugging the AC into the wall socket.

systemctl · the full picture

Two questions, two commands

is-active asks 'now?', is-enabled asks 'at boot?'. Check both — a service can be any of the four combinations.

ops@vps — zsh
$ sudo systemctl enable app
Created symlink /etc/systemd/system/multi-user.target.wants/app.service → /etc/systemd/system/app.service.
$ systemctl is-active app
active
$ systemctl is-enabled app
enabled
active = running right now · enabled = will start at boot
systemctl · cheat sheet

The commands you'll actually use

Everything else is rare. These become muscle memory within a week.

GoalCommand
Is it running? what's wrong?systemctl status app
Restart after a deploysudo systemctl restart app
Start it right nowsudo systemctl start app
Start at every bootsudo systemctl enable app
Never auto-start againsudo systemctl disable app
Yes/no checks for scriptssystemctl is-active app
systemctl · restart

restart: what it does, when you press it

What it does

Stops the service, then starts it fresh. There's a brief gap — a few in-flight requests may drop. That's expected and normal.

The process gets a new PID; old connections don't survive it.

When you press it

After a deploy (new code), after a config change, or when status looks odd even though the service is still 'active'.

If the fix is a config file the app only reads at startup, restart IS the apply button.

03
Part 3

Unit files — the instruction sheet

Plain text, three sections, a handful of lines that matter. You'll write them by hand or with AI — but you must be able to read them.

Unit files · the whole thing

A complete service, in 14 lines

This is the entire unit file for our Flask app. Every line you'll ever need is in it.

app.service
[Unit]
Description=My Flask App
After=network.target

[Service]
User=www-data
WorkingDirectory=/opt/app
ExecStart=/usr/bin/python3 /opt/app/server.py
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target

[Unit] = who I am · [Service] = how to run me · [Install] = when to start me

Unit files · section 1

[Unit] — who I am

Metadata and ordering: how systemd talks about this service, and when it may start.

Description=

A human label shown in every status output. The first thing you'll read in every incident.

After=

Ordering: start me after these (After=network.target = the network is up). It's order, not a hard dependency.

The section you rarely touch

For our purposes, Description + After=network.target is the whole section. Copy it from a working example.

Unit files · section 2

[Service] — how to run me

The heart of the file: the actual command and the environment it runs in.

ExecStart=

The exact command, with absolute paths. Relative paths don't work — a server has no 'current folder'.

User=

Who runs it. Never root for an app — www-data or a dedicated user limits the blast radius of any bug.

WorkingDirectory=

Where the process starts — matters if your code opens relative paths or writes local files.

Environment=

Variables the process sees: FLASK_ENV=production, DATABASE_URL=… AI loves generating these — you must know what they do.

Unit files · section 3

[Install] — when to start me

One line does all the work. It's what 'enable' reads.

WantedBy=multi-user.target

'Start me whenever the system reaches normal multi-user state' = at boot. That's the whole line you'll ever use.

What enable does with it

enable app reads WantedBy and creates a symlink inside multi-user.target.wants/ — that symlink IS 'start at boot'.

Unit files · the workflow

Edit → reload → enable → verify

systemd keeps the unit files in memory. After every edit, you must tell it to re-read — forget that and your edit is invisible.

  1. 1

    Write or edit the unit file

    sudo nano /etc/systemd/system/app.service

  2. 2

    Tell systemd to re-read

    sudo systemctl daemon-reload — without this, systemd still runs the old instructions.

  3. 3

    Enable (and start)

    sudo systemctl enable --now app — enable for every boot, --now starts it immediately.

  4. 4

    Verify

    systemctl status app — green 'active (running)' is the goal.

Unit files · the locations

Two folders, one rule: /etc wins

/etc/systemd/system/

Yours. Admin-edited files, including the app.service you wrote. This is where you create and modify services.

If a name exists in both folders, this one wins.

/lib/systemd/system/

Theirs. Ships with every installed package — nginx.service, sshd.service, postgresql.service live here, pre-written.

Usually you only read these. Overriding one = copying it to /etc and editing the copy.

04
Part 4

journalctl — the log window

Every service's stdout and stderr lands in one system-wide journal. One command opens the window to any of them.

journalctl · the idea

The journal: one log for everything

Everything is captured

Your app prints to stdout or stderr? It's in the journal. So is every other service — no scattered log files to hunt down.

Indexed per service

journalctl -u app filters to one service — as if each had its own log file, without any file existing.

It's how you debug

A service is down? Its last words are in the journal, a few keystrokes away from you.

journalctl · one service

One service's whole story

ops@vps — zsh
$ journalctl -u app -n 30
Oct 26 09:12:01 vps systemd[1]: Started app.service - My Flask App.
Oct 26 09:12:01 vps app[1042]: * Serving Flask app 'server'
Oct 26 09:12:01 vps app[1042]: * Running on http://0.0.0.0:5000
Oct 27 02:41:07 vps app[1042]: 10.0.0.5 - GET /api/orders 200 312 0.0421s
Oct 27 03:02:19 vps app[1042]: ERROR: connection refused: database
Oct 27 03:02:19 vps app[1042]: Traceback (most recent call last):
-u app = only this service · -n 30 = the last 30 lines
journalctl · live

Follow it live

-f = follow: new lines stream in as they happen. The ops equivalent of tail -f — while you deploy, watch it come up.

ops@vps — zsh
$ journalctl -u app -f
Following log of app.service (press Ctrl+C to stop)…
Oct 27 09:14:02 vps systemd[1]: Started app.service - My Flask App.
Oct 27 09:14:02 vps app[2210]: * Serving Flask app 'server'
Oct 27 09:14:02 vps app[2210]: * Running on http://0.0.0.0:5000
Oct 27 09:14:05 vps app[2210]: 10.0.0.5 - GET / 200 1024 0.0031s
watching the restart land, line by line
journalctl · cheat sheet

Six flags, every debugging session

What you wantThe flag
Only one service-u app
The last N lines, not the whole history-n 50
Follow new lines live-f
Since a moment in time--since "10 min ago"
Errors only-p err
Jump straight to the end, page with arrows-e
05
Part 5

Keep-alive — never stay dead

The single line that separates 'died at 3 a.m.' from 'back up at 3 a.m. + 2 seconds'.

keep-alive · the policy

Restart= — the resurrection switch

By default, a crashed process stays dead — exactly like your terminal program. One line changes that.

Restart=always

Always bring it back, no matter how it died. The default choice for anything that should be up.

Restart=on-failure

Bring it back only if it died badly (error exit, crash signal). Cleaner — but 'always' is the safe habit.

Restart=on-abnormal

Only on signals or timeouts. You'll rarely need the fine-grained levels.

Restart=no

The default. Stays down. For a service that should be up, that's an outage waiting to happen.

RestartSec=2

Wait 2 seconds between resurrection attempts, so a crash loop can't spin the CPU.

keep-alive · watch it happen

It dies at 3 a.m. — and comes back

Kill the process (simulating a crash). systemd notices within a second and starts it again. The journal tells the whole story.

ops@vps — zsh
$ sudo kill -9 2210 # simulate a crash
$ journalctl -u app -f
Oct 27 03:00:00 vps systemd[1]: app.service: Main process exited, code=killed, status=9/KILL
Oct 27 03:00:00 vps systemd[1]: app.service: Failed with result 'signal'.
Oct 27 03:00:02 vps systemd[1]: Started app.service - My Flask App.
Oct 27 03:00:02 vps app[2288]: * Running on http://0.0.0.0:5000
2 seconds from death to serving — nobody was watching
keep-alive · the daily loop

The 60-second incident loop

Something is down. You have five commands and about a minute. This is the entire job, nine times out of ten.

  1. 1

    Confirm it's down

    systemctl status app — 'failed' or 'inactive' confirms it.

  2. 2

    Read its last words

    journalctl -u app -n 50 — the traceback, the error, the reason.

  3. 3

    Fix the cause

    Bad deploy? Roll back. Missing env var? Add it to the unit file, then daemon-reload.

  4. 4

    Bring it back

    sudo systemctl restart app

  5. 5

    Verify and watch

    systemctl status app, then journalctl -u app -f while you send a test request.

Big picture · the whole story

The supervisor that keeps your site alive

Two ways a service can die. systemd handles both, automatically — so the site stays up.

🔌 Reboot

update, power blip — the machine restarts

💥 Crash

a bug kills the process

▶

👑 systemd

enabled → starts it at boot

Restart=always → revives it on crash

▶

🌐 Your site

up · up · up — 24/7

you deploy code — systemd keeps it breathing across reboots and crashes

Big picture · out of scope

What we're deliberately skipping

systemd is a deep toolbox. These are real features — and not your next step.

Targets & wants

The formal dependency graph (multi-user.target, Wants/Requires). After= + WantedBy already covers what you need.

Timers & sockets

systemd timers (a cron replacement) and socket activation — powerful, and cron is fine for now.

networkd & cgroups

Managing the network stack and resource limits from systemd. Real and deep — not the mental model you need first.

Docker vs systemd

Containers have their own restart policy — the mental model is identical, the commands differ. Covered in the Docker topic.

You write the code. You deploy the code. systemd makes sure the code keeps running — through reboots, crashes, and 3 a.m. You ship; it supervises.
the operator's view

Site up. You asleep.

status → journalctl → restart: the daily loop is in your hands. Next: putting the whole stack together in one deployment.