no matches found.
#line editing
posixCtrl+W
Delete word before cursor. Stop holding backspace like it owes you money.
Ctrl+U
Cut from cursor to start of line.
Ctrl+Y yanks it back.Ctrl+K
Cut from cursor to end of line. Same yank to restore.
Ctrl+Y
Yank (paste) the last thing you cut with
Ctrl+U or Ctrl+K.Ctrl+A
Jump to start of line. Home key, but on the home row.
Ctrl+E
Jump to end of line.
Alt+B
Move back one word. Mac: set Option as Meta in terminal prefs.
Alt+F
Move forward one word.
Ctrl+L
Clear screen without interrupting what you're typing.
Alt+D
Delete word forward from cursor.
Ctrl+T
Swap the two characters before cursor. Fixes typos without backspacing.
Alt+T
Swap the two words before cursor. The word-level version of
Ctrl+T.#process control
posix bash/zshCtrl+C
Kill current foreground process immediately. SIGINT.
Ctrl+D
EOF signal. Closes stdin. On empty prompt, logs you out. Don't press it by accident in production SSH.
Ctrl+Z
Suspend (pause) the foreground process. SIGTSTP. Doesn't kill it.
bg
Resume a suspended process in the background. Your terminal is free again.
fg
Bring a background job back to the foreground.
fg %2 to target a specific job number.jobs -l
List all background jobs with their PIDs and status.
disown
Detach a background job from the shell entirely. SSH can disconnect and the process lives on. The full flow:
Ctrl+Z → bg → disown.nohup cmd &
Run from the start immune to hangups. Output goes to
nohup.out. Preferred when you know in advance.kill -9 PID
SIGKILL. Can't be caught or ignored. The nuclear option. Try
kill PID (SIGTERM) first — give it a chance to clean up.pkill -f nginx
Kill by process name instead of PID.
-f matches against the full command string.#file tricks
posix> file.txt
Truncate a file to zero bytes without deleting it. Preserves permissions, ownership, and open file handles. Cleaner than
rm && touch.cp nginx.conf{,.bak}
Quick backup in one shot. Expands to
cp nginx.conf nginx.conf.bak.ln -s /real/path ./link
Create a symlink.
-s for symbolic (the one you want 99% of the time). Omit for a hard link.find . -name "*.log" -mtime +30 -delete
Delete log files older than 30 days. Remove
-delete first to preview what it would nuke.diff <(sort f1) <(sort f2)
Process substitution. Treat command output as a file. No temp files, no cleanup.
du -sh */ | sort -rh
Show size of each subdirectory, sorted largest first. Useful for finding where your disk went.
rsync -avz --progress src/ dest/
Sync directories.
-a preserves everything, -z compresses in transit, --progress because you want to watch.tar -czvf out.tar.gz ./dir
tar -xzvf out.tar.gz
tar -xzvf out.tar.gz
Create and extract gzipped tarballs.
c=create, x=extract, z=gzip, v=verbose, f=filename.stat file.txt
Full metadata dump: size, permissions, inode, timestamps, device. More detail than
ls -l will ever give you.file mystery.dat
Identify file type by content, not extension. Useful when someone sends you a "document" with no extension.
#text processing
posixgrep -rn "pattern" .
Recursive search with line numbers. The first thing you reach for when something breaks and you don't know where.
grep -v "exclude"
Invert match. Show lines that don't contain the pattern. Chain multiple
-v pipes for exclusion filters.sort file | uniq -c | sort -rn
Count occurrences of each line, sorted by frequency. The quick-and-dirty frequency analysis.
wc -l file
Count lines.
-w for words, -c for bytes. Pipe anything into it: git diff --stat | wc -l.cut -d: -f1 /etc/passwd
Extract fields from delimited data.
-d sets delimiter, -f selects fields. Combine: -f1,3 or -f1-4.tail -f /var/log/syslog
Follow a log file in real time.
head -n 20 for first N lines, tail -n 20 for last N. The -f flag is why tail exists.tr '[:upper:]' '[:lower:]'
Transform characters. Also:
tr -d '\r' to strip Windows line endings, tr -s ' ' to squeeze repeated spaces.column -t -s, file.csv
Format delimited input into aligned columns. Makes CSV dumps actually readable in the terminal.
curl -s api/endpoint | jq '.data[]'
Parse and filter JSON.
.key extracts a field, .[] iterates arrays, select(.active) filters. Pipe any API response through it.diff -u file1 file2
Unified diff format.
-u gives context lines (like git diff). --color to make it readable. colordiff if available.#scripting safety
posixset -e
Exit on error. Has notorious edge cases inside conditionals, pipes, and while loops. Don't treat it as a full safety net.
set -u
Treat unset variables as errors. Protects you from typos that expand to nothing and silently delete things they shouldn't.
set -euo pipefail
The standard safety header. Put it after your shebang.
-o pipefail makes pipeline failures visible. Learn its caveats before trusting it blindly.cmd |& tee output.log
Capture both stdout and stderr to a file while still watching output live.
|& is shorthand for 2>&1 |. Works in bash/zsh; use explicit form for POSIX sh.shellcheck script.sh
Static analysis for shell scripts. Catches quoting bugs, undefined variables, and POSIX portability issues. Run it before you run the script.
#history & recall
bash/zshCtrl+R
Reverse incremental history search. Type part of a command, hit again to cycle back. Stop pressing the up arrow 40 times.
Alt+.
Insert last argument of previous command. Hit again to cycle further back through history.
sudo !!
Rerun the previous command with sudo. The "permission denied" walk of shame, fixed.
!$
Expands to the last argument of the previous command at execution time. Unlike
Alt+., no preview — it fires when you hit Enter.Ctrl+X Ctrl+E
Drop current command into
$EDITOR. When your one-liner becomes a four-liner, use an actual editor.fc
Open previous command in
$EDITOR. More portable than Ctrl+X Ctrl+E. Works across most shells.history | grep rsync
Search your command history. Pair with
tail -n 50 to limit to recent commands.HISTCONTROL=ignoredups:erasedups
Put in
~/.bashrc. Stops duplicate commands from polluting your history. HISTSIZE=10000 while you're in there.#expansion & globbing
bash/zshcp config.yml{,.bak}
Brace expansion for backups. Expands to
cp config.yml config.yml.bak.mv filename.{txt,md}
Rename extension in one command. Expands to
mv filename.txt filename.md.mkdir -p project/{src,tests,docs,dist}
Create multiple directories at once.
shopt -s globstar
ls **/*.js
ls **/*.js
Enable globstar to match files recursively across all subdirectories. Enabled by default in Zsh. Often faster than reaching for
find.for i in {1..10}; do echo $i; done
Brace expansion with ranges. Also works with letters:
{a..z}.echo "today: $(date +%F)"
Command substitution. Prefer
$(...) over backticks — it nests cleanly.${VAR:-default}
Use a default if variable is unset or empty.
${VAR:=default} also assigns. ${VAR:?error msg} aborts with an error.#advanced shell
bash/zshdiff <(cmd1) <(cmd2)
Process substitution. Treats command output as if it were a file. No temp files, no cleanup.
cat <<EOF
line one
line two
EOF
line one
line two
EOF
Heredoc. Feed multiline input to a command inline. Useful in scripts to write config files or SQL without temp files.
trap 'rm -f /tmp/work.$$' EXIT
Run cleanup on script exit, even if it crashes.
$$ is the script's PID — good for namespacing temp files.find . -name "*.log" | xargs rm
Pass output as arguments. Safer with
-print0 | xargs -0 for filenames with spaces. -P4 runs 4 in parallel.awk -F: '{print $1}' /etc/passwd
Print first field of a colon-delimited file.
$NF for last field, NR for line number.sed -i 's/old/new/g' file.txt
In-place find and replace. On macOS,
sed -i '' (empty string required). Add I flag for case-insensitive: s/old/new/gI.watch -n 2 'df -h'
Repeat a command every N seconds. Useful for watching log growth, disk usage, or process status without tailing.
curl -sL -o /dev/null -w "%{http_code}" URL
Just check the HTTP status code, suppress output.
-sL = silent + follow redirects. Swap in -H "Authorization: Bearer TOKEN" for APIs.while IFS= read -r line; do
echo "$line"
done < file.txt
echo "$line"
done < file.txt
Read a file line by line.
IFS= preserves whitespace, -r prevents backslash interpretation. The correct way — for line in $(cat file) is wrong.#nano
nanoCtrl+O
Save (Write Out). Prompts for filename. Hit Enter to confirm. The thing everyone looks for first.
Ctrl+X
Exit. If you have unsaved changes, it'll ask.
Y to save, N to discard, Ctrl+C to cancel.Ctrl+W
Search.
Ctrl+W again to repeat. Alt+W searches backward.Ctrl+\
Find and replace. Prompts for search string, then replacement.
A to replace all.Ctrl+K
Cut current line (or selected text) to buffer. Cut multiple lines, then paste all at once with
Ctrl+U.Ctrl+U
Paste (Uncut). Inserts whatever is in the cut buffer at cursor position.
Ctrl+_
Go to line (and column). Type the line number and hit Enter. The underscore is shift+minus.
Alt+U
Undo.
Alt+E to redo. Available in nano 2.4+, which is most modern systems.Alt+A
Set mark (start selection) at cursor. Move cursor to extend selection, then
Ctrl+K to cut or Alt+6 to copy.Alt+6
Copy selected text to buffer without cutting it.
nano -l file.txt
Open with line numbers displayed. Or set
set linenumbers in ~/.nanorc to make it permanent.~/.nanorc
Config file. Useful entries:
set linenumbers, set autoindent, set tabsize 4, set mouse. Include syntax files from /usr/share/nano/.#git
gitgit status -sb
Short status with branch info. Much less noise than plain
git status.git log --oneline --graph --decorate
Compact history with branch topology. Add
--all to see all branches.git diff --staged
See what's staged but not yet committed. Plain
git diff only shows unstaged changes.git stash
git stash pop
git stash pop
Stash dirty working state, do something else, restore it.
git stash list to see what's saved. git stash apply stash@{2} for a specific one.git commit --amend --no-edit
Add staged changes to the last commit without changing the message. Only use this before pushing.
git reset --soft HEAD~1
Undo last commit, keep changes staged.
--mixed = unstaged, --hard = gone. HEAD~2 for two commits back.git rebase -i HEAD~3
Interactive rebase on last 3 commits. Squash, reorder, edit, or drop commits.
fixup merges into previous without message.git cherry-pick <hash>
Apply a specific commit from another branch onto the current one. Useful for pulling in a fix without merging the whole branch.
git fetch --prune
Fetch from remote and delete local references to remote branches that no longer exist. Keeps things tidy.
git branch -d branch-name
git push origin --delete branch-name
git push origin --delete branch-name
Delete a branch locally and on the remote.
-d is safe (won't delete unmerged). -D forces it.git bisect start
git bisect bad HEAD
git bisect good v1.0
git bisect bad HEAD
git bisect good v1.0
Binary search through history to find which commit introduced a bug. Git checks out commits, you mark good/bad, it narrows down.
git blame -L 10,20 file.py
Show who last touched each line.
-L limits to a line range so you're not scrolling forever.git worktree add ../hotfix hotfix-branch
Check out another branch in a separate directory without stashing. Work on two branches simultaneously from one repo.
git config --global alias.lg "log --oneline --graph"
Create a git alias. Now
git lg is your compact log. Store in ~/.gitconfig.git reflog
The panic button. Shows every move HEAD has made, including commits you thought you deleted. Recover with
git checkout <hash>.git clean -fd
Delete all untracked files and directories.
-n for a dry run first. Can't be undone without a stash.#ssh
sshssh-keygen -t ed25519 -C "label"
Generate a modern key pair. Ed25519 is shorter, faster, and more secure than RSA. No reason to use RSA for new keys.
ssh-copy-id user@host
Install your public key on a remote server. Does the
~/.ssh/authorized_keys dance for you. -i to specify which key.ssh -L 8080:localhost:3000 host
Local port forward. Access
localhost:8080 on your machine, traffic tunnels through SSH to port 3000 on the remote. Punches through firewalls.ssh -R 8080:localhost:3000 host
Remote port forward. Expose your local port 3000 on the remote server's port 8080. Share a local dev server through an SSH tunnel.
ssh -D 1080 host
SOCKS proxy. Route browser traffic through the SSH connection. Point your browser proxy settings at
localhost:1080.ssh -J bastion target
ProxyJump through a bastion host. Chain connections without opening ports on the final target.
~/.ssh/config
Host prod
HostName 10.0.0.5
User deploy
IdentityFile ~/.ssh/deploy_ed25519
Port 2222
Stop typing full connection strings. Name your servers.
ssh prod expands everything.eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519
ssh-add ~/.ssh/id_ed25519
Agent caches your key passphrase for the session. Type it once, not every time you connect. Most desktop environments start this automatically.
scp file user@host:/path/
sftp user@host
sftp user@host
scp for quick one-off transfers, sftp for interactive browsing. For anything serious, use rsync.ssh host 'cat /var/log/auth.log' | grep Failed
Run a command remotely without opening an interactive session. Pipe the output locally. Combine with shell pipes for remote+local processing.
#tmux
tmuxtmux new -s name
New named session. Always name your sessions.
tmux new -s deploy beats tmux followed by "which session was that?"tmux attach -t name
Reattach to a running session.
-d to detach other clients first. Also tmux a as shorthand.tmux ls
List all sessions with attached/detached status. Quick check of what's running.
Ctrl+B d
Detach from current session. The session keeps running. This is why tmux exists — SSH dies, your work doesn't.
Ctrl+B %
Split pane vertically (left/right).
Ctrl+B "
Split pane horizontally (top/bottom).
Ctrl+B ←↑↓→
Move between panes. Hold Ctrl+B then arrow direction.
Ctrl+B c
New window.
Ctrl+B n / Ctrl+B p to cycle. Ctrl+B 0-9 to jump by number.Ctrl+B z
Zoom pane to full screen. Hit again to unzoom. Useful when a pane is too small to read.
Ctrl+B [
Enter scroll/copy mode. Arrow keys or Page Up/Down to scroll.
q to exit. This is how you scroll in tmux.tmux kill-session -t name
Kill a session and all its windows/panes.
tmux kill-server for the nuclear option.Ctrl+B ,
Rename current window.
Ctrl+B $ to rename the session. Name things so you don't have to count window numbers.#systemd
systemdsystemctl status nginx
Check service status, recent logs, and PID. The first thing to run when something isn't working.
systemctl restart nginx
Control a service.
restart does stop then start. reload if the service supports it (nginx does, your app might not).systemctl enable --now nginx
Start on boot AND start immediately. Without
--now, it only takes effect next boot. disable to undo.journalctl -u nginx -f
Follow logs for a specific service. Like
tail -f but for systemd units. -e to jump to end without following.journalctl --since "1 hour ago"
Time-filtered logs. Also accepts
"2025-01-15 14:00" or "yesterday". Add --until for a range.systemctl list-units --failed
Find broken services. Run this first when the server feels wrong.
systemctl daemon-reload
Reload unit file definitions after editing them. Required before
restart will pick up changes to .service files.systemd-analyze blame
Show how long each service took to start during boot. Find the bottleneck.
custom unit file
[Unit]
Description=My App
After=network.target
[Service]
ExecStart=/usr/local/bin/myapp
Restart=on-failure
User=www-data
WorkingDirectory=/opt/myapp
[Install]
WantedBy=multi-user.target
Save to
/etc/systemd/system/myapp.service. Then daemon-reload and enable --now. Restart=on-failure is the sane default.journalctl --vacuum-size=500M
Shrink journal logs to 500MB. Also
--vacuum-time=7d to keep only the last week. Journals grow silently — check journalctl --disk-usage.#users & permissions
posixchmod 644 file
chmod 755 dir
chmod 755 dir
The two permissions you'll use 90% of the time. 644 = owner read-write, everyone read. 755 = same but executable.
chmod +x script.sh
Make a file executable. Symbolic mode is cleaner than numeric for single changes.
chown user:group file
Change ownership.
-R for recursive. Most "permission denied" server issues end here.useradd -m -s /bin/bash user
Create a user with a home directory and a real shell. Without
-m, no home dir. Without -s, they might get /bin/sh or nothing.usermod -aG sudo user
Add user to sudo group. The
-a is critical — without it, you replace all their groups instead of appending. Ask me how I know.passwd user
Set or change a user's password. Root can change anyone's. Users can change their own.
id
Show current user, UID, and all groups.
id username for someone else. whoami for just the name.visudo
Edit sudoers file safely. Syntax-checks before saving. Never edit
/etc/sudoers directly — a typo locks everyone out of sudo.#networking
posixss -tlnp
Show listening TCP ports with process names. Replaced
netstat. -u for UDP. First thing to check when "port already in use."ip addr
Show network interfaces and IPs. Replaced
ifconfig. ip link for interface status only.dig example.com +short
DNS lookup.
+short for just the IP. @8.8.8.8 to query a specific resolver. Without +short, shows full answer with TTL.curl -I https://example.com
Fetch headers only. Quick way to check HTTP status, redirects, and server headers without downloading the body.
ping -c 4 host
Basic connectivity test.
-c 4 sends 4 packets and stops. Without it, ping runs forever (and you Ctrl+C in a panic).traceroute host
Show the network path to a host. Useful for diagnosing where packets are getting dropped.
mtr is the interactive version — install it.nc -zv host 443
Test if a port is open. Faster than curl for raw port checks.
-z scans without sending data, -v for verbose.ip route
Show routing table.
ip route get 8.8.8.8 to see which interface and gateway a specific destination would use.lsof -i :8080
Show what process is using a port. More detail than
ss. Also: lsof +D /var/log to find what has files open in a directory.resolvectl status
Show DNS resolver configuration on systemd systems. Tells you which DNS servers you're actually using per interface.
#firewall (ufw)
posixufw allow 22/tcp && ufw enable
Turn on the firewall. Allow SSH first — or you'll lock yourself out. Defaults to deny incoming, allow outgoing.
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 443/tcp
Allow web traffic. Specify
/tcp or /udp. Without a protocol, it allows both.ufw status verbose
Show all rules and default policies. The
verbose part matters — it shows the default deny/allow policies.ufw deny from 203.0.113.0/24
Block an IP range. Useful for banning persistent scanners.
ufw status numbered
ufw delete 3
ufw delete 3
Remove a rule by number.
ufw status numbered first to see which is which. Also: ufw delete allow 8080/tcp.ufw app list
Show application profiles.
ufw allow 'Nginx Full' is cleaner than specifying ports. ufw app info 'Nginx Full' to see what it opens.#nginx
nginxnginx -t && systemctl reload nginx
Always test config before reloading.
-t validates and exits. Only reload if it passes — && ensures this.static site block
server {
listen 443 ssl http2;
server_name example.com;
root /var/www/example;
index index.html;
location / {
try_files $uri $uri/ =404;
autoindex off;
}
}
Basic static site block.
autoindex off stops nginx showing directory listings. try_files before 404 catches clean URLs.security headers
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "no-referrer";
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
Add to your
server {} block. These cost nothing and kill a category of attacks. Aim for A on securityheaders.com.force https redirect
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
Permanent redirect from HTTP to HTTPS. Keep this as a separate server block — don't mix it into your SSL block.
certbot ssl
certbot --nginx -d example.com -d www.example.com
Issue and install a Let's Encrypt cert. Certbot modifies your nginx config automatically. Renews via systemd timer or cron.
reverse proxy
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Proxy to a local app server. The
X-Forwarded-* headers let your app know the real client IP and protocol.block dotfiles
location ~ /\.(?!well-known).* {
deny all;
}
Block access to
.git, .env, and any other dotfiles. The well-known exception is for Let's Encrypt validation.gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json
application/javascript text/xml application/xml;
Add in
http {} block. Reduces payload sizes significantly for text assets. gzip_vary adds the Vary: Accept-Encoding header for proper caching.basic rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
}
10 requests/second per IP with a burst of 20. Define the zone in
http {}, apply it in location {}.log location
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log warn;
Per-vhost log files.
warn level skips the noise. Tail with tail -f or set up fail2ban against the access log.websocket proxy
location /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
WebSocket support requires HTTP/1.1 upgrade headers. Without these, the connection silently falls back to regular HTTP and your websocket never connects.
#docker
dockerdocker ps
List running containers.
-a to include stopped ones. The "what's running" command.docker run -d -p 8080:80 --name app nginx
-d detaches, -p maps host:container ports, --name gives it a human name so you don't have to reference hex IDs.docker logs -f container
Follow container logs.
--tail 50 to start from the last 50 lines instead of dumping everything since creation.docker exec -it container /bin/sh
Open a shell inside a running container.
-it for interactive terminal. Use bash if available, sh as fallback.docker compose up -d
Start all services defined in
docker-compose.yml. -d for detached. The modern command drops the hyphen.docker compose down
Stop and remove all containers and networks. Add
-v to also remove volumes — careful, that's your data.docker system prune -a
Nuke everything unused: containers, images, networks, build cache. Reclaim disk space when Docker ate your drive.
docker build -t name:tag .
Build an image from a Dockerfile in the current directory. Tag it properly or you'll have 47 unnamed images.
docker volume ls
List persistent volumes.
docker volume inspect name to see where it lives on disk. Volumes survive container removal. Bind mounts are the alternative.docker stop container && docker rm container
Graceful shutdown then removal.
docker rm -f does both in one step (SIGKILL, not graceful). docker container prune to remove all stopped.docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container
Get a container's IP address.
docker inspect returns everything as JSON — use Go templates or pipe through jq.dockerfile example
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Minimal Node.js image. Alpine keeps it small.
COPY package*.json first to cache the npm ci layer. Ordering matters for build cache.#gitea
giteagitea admin user create \
--username terje \
--password secret \
--email t@example.com \
--admin
--username terje \
--password secret \
--email t@example.com \
--admin
Create an admin user from the command line. Useful during initial setup or when locked out of the web UI.
gitea admin user change-password \
--username terje \
--password newpassword
--username terje \
--password newpassword
Reset a user password via CLI. Works even when the user can't log in.
gitea admin regenerate hooks
Regenerate git hooks for all repositories. Run this after migrations or if webhooks stop firing.
gitea dump -c /etc/gitea/app.ini
Dump a full backup: repos, database, config, attachments. Creates a zip. Schedule this in cron and point it somewhere not on the same disk.
key config paths
/etc/gitea/app.ini # main config
/var/lib/gitea/data/ # repos + data
/var/lib/gitea/repositories/ # bare git repos
Know where these live before you need them in an emergency.
systemctl status gitea
systemctl restart gitea
journalctl -u gitea -f
systemctl restart gitea
journalctl -u gitea -f
Standard systemd controls.
journalctl -f tails live logs — useful when debugging startup failures.gitea admin repo migrate \
--url https://github.com/user/repo
--url https://github.com/user/repo
Migrate a repository from an external source. Also available via the web UI under the migration section — usually easier.
gitea admin user generate-access-token \
--username terje \
--token-name deploy
--username terje \
--token-name deploy
Generate an API/deploy token for a user. Useful for CI pipelines and automated scripts that need to push or pull.
nginx proxy config for gitea
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 512M;
}
Proxy nginx to Gitea. The
client_max_body_size matters — without it, large repo pushes will fail with a 413.smtp config in app.ini
[mailer]
ENABLED = true
SMTP_ADDR = mail.example.com
SMTP_PORT = 587
FROM = gitea@example.com
USER = gitea@example.com
PASSWD = yourpassword
IS_TLS_ENABLED = false
STARTTLS_ENDPOINT = true
Email configuration. Test with
gitea admin sendmail --to test@example.com --subject test after saving.