Installing¶
Docker edition¶
Assuming Docker and Docker Compose are already installed.
For now, there’s no image published on Docker Hub, this means you will have to build the image locally.
Clone the repository, replace you-domain.tld by your own domain.
Note that if you want to serve static assets via your reverse proxy (like nginx), clone it in a place where it is accessible by your reverse proxy user.
git clone https://github.com/toniher/microblog.pub your-domain.tld
Build the Docker image locally.
make build
Run the configuration wizard.
make config
Update data/profile.toml and add this line in order to process headers from the reverse proxy:
trusted_hosts = ["*"]
Start the app with Docker Compose, it will listen on port 8000 by default.
The port can be tweaked in the docker-compose.yml file.
docker compose up -d
Setup a reverse proxy (see the Reverse Proxy section).
What runs inside the container¶
The image is built from python:3.12-slim and a single container runs four
processes under supervisord (see misc/docker-supervisord.conf):
uvicorn— the web server, listening on0.0.0.0:8000incoming_worker— processes incoming federation activities (your inbox)outgoing_worker— delivers your outgoing activities to other serverspush_worker— delivers Web Push notifications to subscribed Mastodon client apps (seedocs/mastodon_api.md)
On every start, the entrypoint (misc/docker_start.sh) first runs inv update --no-update-deps, which recompiles the CSS, compiles the translation catalogs
(.po → .mo, see Translations / i18n),
and applies any pending database migrations before launching supervisord. You
therefore don’t need to run migrations by hand after pulling a new version —
restarting the container is enough.
The container runs as the unprivileged user 1000:1000 (see the user: line in
docker-compose.yml), and its state lives in two volumes so it survives image
rebuilds:
./data→/app/data— a host bind mount holding your config (profile.toml), secrets, the SQLite database, uploads and logs. Keep this backed up.microblogpub_static→/app/app/static— a named Docker volume for generated assets: compiled CSS, the favicon, the downloaded Twemoji set and custom emoji.
Using a named volume (rather than a host bind mount) for app/static means the
image ships a pristine copy of the static assets, and the entrypoint
(misc/docker_start.sh) repopulates the volume from that copy whenever it is
empty — restoring the base assets and recompiling the CSS. So a removed volume is
transparently rebuilt on the next start with no manual step:
docker compose down
docker volume rm microblogpub_static # recreated & repopulated on next up
docker compose up -d
The Twemoji set is handled separately: the entrypoint re-downloads it on every container start (needs network access), so it’s always complete and current regardless of the volume’s state — including after a wipe. Every boot is therefore slightly slower because of the ~4,000-file Twemoji download.
Managing the app¶
docker compose ps # show the container status
docker compose stop # stop the app
docker compose up -d # (re)start the app in the background
docker compose restart # restart (e.g. after editing data/profile.toml)
Note that most configuration changes (anything in data/profile.toml) only take
effect after a restart.
Viewing logs¶
supervisord writes each process’ output to a file under data/, which you can tail
from the host:
tail -f data/uvicorn.log # web server
tail -f data/incoming.log # incoming federation worker
tail -f data/outgoing.log # outgoing federation worker
tail -f data/push.log # Web Push delivery worker
supervisord rotates each of these once it hits stdout_logfile_maxbytes (see
misc/*supervisord.conf), producing data/*.log.1, data/*.log.2, etc. — but it
never compresses the backups. misc/gzip_rotated_logs.sh gzips those rotated
files; run it periodically from the host’s crontab (or a systemd timer) against
the data/ directory, e.g.:
0 3 * * * /path/to/repo/misc/gzip_rotated_logs.sh /path/to/data
The container’s own stdout/stderr is also available via Docker:
docker compose logs -f
Running maintenance tasks¶
Administrative tasks (checking the config, resetting the password, pruning old data,
moving instances, importing follows, …) are exposed as make targets that each spin
up a throwaway container sharing your data/ and app/static/ volumes. For example:
make check-config # validate data/profile.toml
make reset-password # set a new admin password
make account=user@other.tld webfinger # resolve a remote actor URL
See the User’s guide for the full list and the details of each task (each one documents its “Docker edition” invocation).
Updating¶
To update microblogpub, pull the latest changes, rebuild the Docker image and restart the process with docker compose.
git pull
make build
docker compose stop
docker compose up -d
As you probably already know, Docker can (and will) eat a lot of disk space, when updating you should prune old images from time to time:
docker image prune -a --filter "until=24h"
Troubleshooting: PermissionError on app/static/ or data/ after updating¶
docker-compose.yml runs the container as an unprivileged user (user: 1000:1000).
If your data/ and app/static/ directories were created (or previously written to)
by a container running as root — e.g. an older setup without the user: line — the
container’s uid 1000 will no longer be able to write to them, and startup tasks like
compile_scss (which regenerates app/static/favicon.ico and the compiled CSS) will
fail with a traceback ending in PermissionError: [Errno 13] Permission denied, and
uvicorn/the worker processes will crash-loop under supervisord.
Check ownership:
stat -c '%u:%g %n' app/static data
If it doesn’t match the uid:gid in docker-compose.yml’s user: line (default 1000:1000),
fix it:
docker compose down
sudo chown -R 1000:1000 ./data ./app/static
docker compose up -d
Python developer edition¶
Assuming you have a working Python 3.10+ environment (Python 3.12 is recommended — it’s what the project is developed and tested against, and what the Docker image ships).
Setup Poetry.
curl -sSL https://install.python-poetry.org | python3 -
Clone the repository.
git clone https://github.com/toniher/microblog.pub testing.microblog.pub
Install deps.
poetry install
Recommended: install ffmpeg (e.g. apt install ffmpeg). It’s an optional runtime
dependency used only to read video/audio metadata, extract a poster frame, and check that an
uploaded video/audio file will actually play in mainstream browsers (see Video and audio
uploads). Without it, video/audio still uploads —
just with no duration, no poster/blurhash, and no compatibility checking at all (nothing is
ever rejected). The Docker image already includes it.
Setup config.
poetry run inv configuration-wizard
Setup the database.
poetry run inv migrate-db
Grab your virtualenv path.
poetry env info
Run the configured processes (web server + workers, see misc/supervisord.conf)
with supervisord.
VENV_DIR=/home/ubuntu/.cache/pypoetry/virtualenvs/microblogpub-chx-y1oE-py3.12 poetry run supervisord -c misc/supervisord.conf -n
Setup a reverse proxy (see the next section).
Updating¶
To update microblogpub locally, pull the remote changes and run the update task to regenerate the CSS, compile the translation catalogs, and run any DB migrations.
git pull
poetry run inv update
This fork carries a number of Alembic migrations beyond upstream — new tables
(scheduled statuses, Web Push subscriptions, muted threads, read markers), extra
columns (media alt text and focal point, cached remote actor counts, account
mutes, per-follow boost/notify options) and several indexes. The
developer guide lists every one of them
in the order they are applied, and shows how to check which are still pending on
a given database (alembic current vs. alembic heads) — useful if you’re moving
a database between this fork and upstream, or just want to know what changed under
the hood.
Reverse proxy¶
You will also want to setup a reverse proxy like NGINX, see uvicorn documentation:
If you don’t have a reverse proxy setup yet, NGINX + certbot is recommended.
server {
# nginx's own cap needs to be at or above the app-level upload limits
# (max_image_upload_size/max_video_upload_size in data/profile.toml,
# 10 MiB/40 MiB by default) — otherwise nginx rejects a large-but-valid
# upload before the app ever sees it.
client_max_body_size 4G;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect off;
proxy_buffering off;
# Needed for the Mastodon streaming API (wss://…/api/v1/streaming,
# app/mastodon/streaming.py) — the default 60s would kill an idle
# WebSocket, read as an unexplained disconnect by the client.
proxy_read_timeout 3600s;
proxy_pass http://localhost:8000;
}
# [...]
}
# This should be outside the `server` block
map $http_upgrade $connection_upgrade {
default upgrade;
# NOT `close`: that forces `Connection: close` on every ordinary
# (non-upgrade) request too, since $http_upgrade is empty for those.
'' '';
}
Optionally, you can serve static files using NGINX directly, with an additional location block.
This will require the NGINX user to have access to the static/ directory.
server {
# [...]
location / {
# [...]
}
location /static {
# path for static files
rewrite ^/static/(.*) /$1 break;
root /path/to/your-domain.tld/app/static/;
expires 1y;
}
# [...]
}
NGINX config tips¶
Enable HTTP2 (which is disabled by default):
server {
# [...]
listen [::]:443 ssl http2;
}
Tweak /etc/nginx/nginx.conf and add gzip compression for ActivityPub responses:
http {
# [...]
gzip_types text/plain text/css application/json application/javascript application/activity+json application/octet-stream;
}
(Advanced) Running on a subdomain¶
It is possible to run microblogpub on a subdomain (sub.domain.tld) while being reachable from the root root domain (domain.tld) using the name@domain.tld handle.
This requires forwarding/proxying requests from the root domain to the subdomain, for example using NGINX:
location /.well-known/webfinger {
add_header Access-Control-Allow-Origin '*';
return 301 https://sub.domain.tld$request_uri;
}
And updating data/profile.toml to specify the root domain as the webfinger domain:
webfinger_domain = "domain.tld"
Once configured correctly, people will be able to follow you using name@domain.tld, while using sub.domain.tld for the web interface.
(Advanced) Running from subpath¶
It is possible to configure microblogpub to run from subpath.
To achieve this, do the following configuration between config and start steps.
i.e. after you run make config or poetry run inv configuration-wizard,
but before you run docker compose up or poetry run supervisord.
Changing this settings on an instance which has some posts or was seen by other instances will likely break links to these posts or federation (i.e. links to your instance, posts and profile from other instances).
The following steps will explain how to configure instance to be available at https://example.com/subdir.
Change them to your actual domain and subdir.
Edit
data/profile.tomlfile, add this line:id = "https://example.com/subdir"
Edit
misc/*-supervisord.conffile which is relevant to you (it depends on how you start microblogpub - if in doubt, do the same change in all of them) - in[program:uvicorn]section, in the line which starts withcommand, add this argument at the very end:--root-path /subdir
Above two steps are enough to configure microblogpub. Next, you also need to configure reverse proxy. It might slightly differ if you plan to have other services running on the same domain, but for NGINX config shown above, the following changes are enough:
Add subdir to location, so location block starts like this:
location /subdir {Add
/at the end ofproxy_passdirective, like this:proxy_pass http://localhost:8000/;
These two changes will instruct NGINX that requests sent to https://example.com/subdir/... should be forwarded to http://localhost:8000/....
Inside
serverblock, add redirects for well-known URLs (add these lines afterclient_max_body_size, remember to replacesubdirwith your actual subdir!):location /.well-known/webfinger { return 301 /subdir$request_uri; } location /.well-known/nodeinfo { return 301 /subdir$request_uri; } location /.well-known/oauth-authorization-server { return 301 /subdir$request_uri; }Optionally, check robots.txt from a running microblogpub instance and integrate it into robots.txt file in the root of your server - remember to prepend
subdirto URLs, so for exampleDisallow: /adminbecomesDisallow: /subdir/admin.
Available tutorial/guides¶
Opalstack, thanks to @defulmere@mastodon.social.