Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c6f20fbc3 | ||
|
|
b88828f92e | ||
|
|
09780d9598 | ||
|
|
c24ed01e0a | ||
|
|
7e1728a037 | ||
|
|
30ef38b143 | ||
|
|
f927bc191c | ||
|
|
f402556380 | ||
|
|
25b6e0575a | ||
|
|
e133b01b00 | ||
|
|
b7145e1323 | ||
|
|
87867b9f30 | ||
|
|
8a7833e8f5 | ||
|
|
4426668518 | ||
|
|
eacf71d676 | ||
|
|
b05814980f | ||
|
|
eb67d6361e | ||
|
|
8062b9d208 | ||
|
|
aa01def998 | ||
|
|
1ad79d40d8 | ||
|
|
7d4f1668a6 | ||
|
|
498231c160 | ||
|
|
898e3003a2 |
@@ -46,8 +46,11 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:latest
|
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:latest
|
||||||
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:${{ gitea.event.head_commit.id }}
|
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:${{ gitea.event.head_commit.id }}
|
||||||
cache-from: type=gha
|
# Registry-backed build cache. The GitHub-hosted 'gha' cache is not
|
||||||
cache-to: type=gha,mode=max
|
# reachable from self-hosted Gitea runners, so store the cache as a
|
||||||
|
# dedicated tag in the same registry instead.
|
||||||
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:buildcache,mode=max
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
@@ -58,9 +61,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
|
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
|
||||||
hostname: 'gitea-actions-blog'
|
hostname: 'gitea-actions-blog'
|
||||||
management-url: 'https://netbird.ahmza.com'
|
management-url: ${{ secrets.NETBIRD_URL }}
|
||||||
|
|
||||||
- name: Deploy to RPIC-Cluster
|
- name: Deploy to RPIC-Cluster
|
||||||
|
# RPIC cluster is temporarily out of service. Don't let its failure
|
||||||
|
# block the Gimli deploy or fail the whole job. Remove this line once
|
||||||
|
# the cluster is back.
|
||||||
|
continue-on-error: true
|
||||||
uses: appleboy/ssh-action@v1
|
uses: appleboy/ssh-action@v1
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.RPIC_HOST }}
|
host: ${{ secrets.RPIC_HOST }}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Caddyfile for the blog behind Cloudflare.
|
||||||
|
#
|
||||||
|
# Topology: Cloudflare (terminates TLS) -> Caddy (HTTP :80) -> SWS (blog:80)
|
||||||
|
#
|
||||||
|
# Cloudflare handles the public HTTPS cert, so Caddy serves plain HTTP on :80
|
||||||
|
# and does NOT need the Cloudflare DNS-01 / automatic-HTTPS setup. Set the
|
||||||
|
# Cloudflare SSL/TLS mode to "Full" so CF->origin is encrypted at the network
|
||||||
|
# layer (e.g. over your tunnel/Netbird), even though this listener is HTTP.
|
||||||
|
#
|
||||||
|
# Copy this file to /etc/caddy/Caddyfile on the caddy_cf host and reload:
|
||||||
|
# docker exec caddy_cf caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
# or restart the container.
|
||||||
|
{
|
||||||
|
# Trust only Cloudflare's edge IPs so real visitor IPs (Cf-Connecting-Ip /
|
||||||
|
# X-Forwarded-For) are honoured only when the request actually comes from
|
||||||
|
# Cloudflare. This prevents header spoofing from any other source.
|
||||||
|
#
|
||||||
|
# The `cloudflare` IP source (WeidiDeng/caddy-cloudflare-ip, baked into the
|
||||||
|
# caddy-cf image) auto-fetches the ranges from cloudflare.com/ips-v4 and
|
||||||
|
# /ips-v6 and refreshes them periodically, so there's no static list to
|
||||||
|
# keep in sync.
|
||||||
|
servers {
|
||||||
|
trusted_proxies cloudflare {
|
||||||
|
interval 12h
|
||||||
|
timeout 15s
|
||||||
|
}
|
||||||
|
# Read the real visitor IP from these headers (only honoured for
|
||||||
|
# requests from trusted_proxies above). Cf-Connecting-Ip is
|
||||||
|
# Cloudflare's single authoritative end-user IP; X-Forwarded-For is
|
||||||
|
# the fallback if a request ever arrives without the CF header.
|
||||||
|
client_ip_headers Cf-Connecting-Ip X-Forwarded-For
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bind to the hostname on the HTTP port only. Because the address has an
|
||||||
|
# explicit ":80" scheme/port, Caddy will NOT try to provision a TLS cert.
|
||||||
|
http://blog.ahmza.com {
|
||||||
|
# THE responder: proxy every request to the blog (SWS) container.
|
||||||
|
# This is what was missing before (requests fell through to a NOP -> blank page).
|
||||||
|
reverse_proxy blog:80
|
||||||
|
|
||||||
|
# Compression happens here (SWS has it disabled on purpose).
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Security headers (single source of truth; SWS has security-headers off).
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
# Drop Caddy's default Server header.
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-4
@@ -3,8 +3,9 @@ WORKDIR /tmp/hugo
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN hugo --gc --minify
|
RUN hugo --gc --minify
|
||||||
|
|
||||||
FROM nginx:alpine-slim
|
FROM ghcr.io/static-web-server/static-web-server:2
|
||||||
WORKDIR /usr/share/nginx/html
|
COPY sws.toml /etc/sws.toml
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY --from=build /tmp/hugo/public /public
|
||||||
COPY --from=build /tmp/hugo/public .
|
ENV SERVER_CONFIG_FILE=/etc/sws.toml
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
|
CMD ["--config-file", "/etc/sws.toml"]
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: { { replace .TranslationBaseName "-" " " | title } }
|
title: {{ replace .TranslationBaseName "-" " " | title }}
|
||||||
description: ""
|
description: ""
|
||||||
tags: [""]
|
tags: []
|
||||||
author: "ahmza"
|
author: "ahmza"
|
||||||
date: { { .Date } }
|
date: {{ .Date }}
|
||||||
draft: true
|
draft: true
|
||||||
weight:
|
weight:
|
||||||
featureimage:
|
featureimage:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 782 KiB After Width: | Height: | Size: 441 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 73 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
blog:
|
blog:
|
||||||
image: ghcr.io/ahmzaa/blog-ahmza-com
|
image: tea.ahmza.com/ahmza/blog-ahmza-com:latest
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ pagination.pagerSize = 10
|
|||||||
buildDrafts = false
|
buildDrafts = false
|
||||||
buildFuture = false
|
buildFuture = false
|
||||||
|
|
||||||
|
# Keep source-art and editor backup files out of the built site. Without this,
|
||||||
|
# files inside page bundles (e.g. feature.aseprite) are copied into public/ as
|
||||||
|
# page resources and served publicly.
|
||||||
|
ignoreFiles = ['\.aseprite$', '\.bak$']
|
||||||
|
|
||||||
enableEmoji = true
|
enableEmoji = true
|
||||||
|
|
||||||
# googleAnalytics = "G-XXXXXXXXX"
|
# googleAnalytics = "G-XXXXXXXXX"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Arrstack
|
title: Arrstack
|
||||||
description:
|
description:
|
||||||
tags: [""]
|
tags: []
|
||||||
author: "ahmza"
|
author: "ahmza"
|
||||||
date: 2025-05-09T23:44:27+01:00
|
date: 2025-05-09T23:44:27+01:00
|
||||||
draft: true
|
draft: true
|
||||||
@@ -4,7 +4,7 @@ description: In the modern age we are overwhelmed with stimuli. Learn to cut out
|
|||||||
tags:
|
tags:
|
||||||
- lifestyle
|
- lifestyle
|
||||||
- minimalism
|
- minimalism
|
||||||
- prodcutivity
|
- productivity
|
||||||
author: ahmza
|
author: ahmza
|
||||||
date: 2024-01-02
|
date: 2024-01-02
|
||||||
draft: false
|
draft: false
|
||||||
|
|||||||
@@ -185,8 +185,8 @@ cd .server-data
|
|||||||
2. **Download a Minecraft server jar file:**
|
2. **Download a Minecraft server jar file:**
|
||||||
|
|
||||||
- [Vanilla Minecraft Server](https://www.minecraft.net/en-us/download/server)
|
- [Vanilla Minecraft Server](https://www.minecraft.net/en-us/download/server)
|
||||||
- Right-click the green link that says minecraft_server.<version>.jar and copy the link address.
|
- Right-click the green link that says `minecraft_server.<version>.jar` and copy the link address.
|
||||||
- Download with: curl -o server.jar <link you copied here>
|
- Download with: `curl -o server.jar <link you copied here>`
|
||||||
- [Fabric Mod loader server](https://fabricmc.net/use/server/)
|
- [Fabric Mod loader server](https://fabricmc.net/use/server/)
|
||||||
- Select the Minecraft version, Fabric Loader Version, and the Installer Version of your choice. I tend to go with the latest.
|
- Select the Minecraft version, Fabric Loader Version, and the Installer Version of your choice. I tend to go with the latest.
|
||||||
- Download with: Copy the curl command under CLI download
|
- Download with: Copy the curl command under CLI download
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
---
|
|
||||||
title: Minecraft Server on Docker ⛏ 🐳
|
|
||||||
date: 2022-05-24
|
|
||||||
lastmod: 2024-07-20
|
|
||||||
author: "ahmza"
|
|
||||||
draft: false
|
|
||||||
weight:
|
|
||||||
|
|
||||||
tags: ["games", "docker"]
|
|
||||||
description: "Ever wanted to build a Minecraft server in docker !! Start here"
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Introduction**
|
|
||||||
|
|
||||||
Minecraft is a great game that becomes even more enjoyable when played with
|
|
||||||
friends. One of the best ways to do this is by setting up a dedicated server.
|
|
||||||
This guide will help you do just that!
|
|
||||||
|
|
||||||
Setting up and running a Minecraft server does not require a lot of resources,
|
|
||||||
but based on my experience, allocating more resources significantly enhances
|
|
||||||
performance. Especially when you start to add mods and increase player count.
|
|
||||||
|
|
||||||
## **Prerequisites**
|
|
||||||
|
|
||||||
- Docker
|
|
||||||
- Docker Compose
|
|
||||||
|
|
||||||
See [Pi Cluster](/posts/pi-docker-swarm/)
|
|
||||||
for more information about my docker setup.
|
|
||||||
|
|
||||||
## **Requirements**
|
|
||||||
_Requirements Gathered from [here](https://minecraft.fandom.com/wiki/Server/Requirements/Dedicated#Unix_.28Linux.2C_BSD.2C_macOS.29)_
|
|
||||||
|
|
||||||
### **Minimum (1-5 Players)**
|
|
||||||
|
|
||||||
- Intel Pentium 4 or AMD Athlon based CPU
|
|
||||||
- 512 MB RAM
|
|
||||||
- 2 GB Storage space
|
|
||||||
|
|
||||||
### **Recommended (10-15 Players)**
|
|
||||||
|
|
||||||
- Intel Core or AMD K8 based CPU
|
|
||||||
- 3 GB RAM
|
|
||||||
- 16 GB Storage Space
|
|
||||||
|
|
||||||
### **Optimal (20+ Players)**
|
|
||||||
|
|
||||||
- Intel i5/i7 or AMD Ryzen 5/7 Based CPU
|
|
||||||
- 6 GB RAM
|
|
||||||
- 35 GB Storage Space
|
|
||||||
|
|
||||||
**My opinion**:
|
|
||||||
|
|
||||||
I personally went with the **optimal** settings as I wanted to run mods as well
|
|
||||||
as have the capacity for ~10 players. This gives the server breathing room.
|
|
||||||
|
|
||||||
However if you will be runing no mods or a few light weight mods and only a few
|
|
||||||
players then there really is no need to go higher than 3-4 GB.
|
|
||||||
|
|
||||||
## **Building the Container**
|
|
||||||
|
|
||||||
Lets get started on building the Minecraft server
|
|
||||||
|
|
||||||
1. **Create a directory for the Minecraft server files**:
|
|
||||||
```bash
|
|
||||||
# Make the folder
|
|
||||||
mkdir minecraft-server
|
|
||||||
# Enter the folder
|
|
||||||
cd minecraft-server
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Create an environment file for the Docker Container**:
|
|
||||||
- The _.env_ file allows us to specify variables used in the build process.
|
|
||||||
```bash
|
|
||||||
# Create a .env file
|
|
||||||
touch .env
|
|
||||||
# Open in a text editor
|
|
||||||
vim .env
|
|
||||||
# Enter the following variables
|
|
||||||
CONTAINER_NAME=minecraft-server
|
|
||||||
SERVER_MEM=4096 #change this to your desired value (in MB)
|
|
||||||
SERVER_JAR=<downloaded-server-file>.jar # Minecraft server jar file. Change this later
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Create the _docker-compose.yml_ file**:
|
|
||||||
```bash
|
|
||||||
# Create the file
|
|
||||||
touch docker-compose.yml`
|
|
||||||
# Open in a text editor
|
|
||||||
vim docker-compose.yml
|
|
||||||
```
|
|
||||||
- _docker-compose.yml_ is the spec of our Minecraft server.
|
|
||||||
- Build a docker image using context . and pass some ARGS.
|
|
||||||
- Attach volume .server-data, where Minecraft's files will be stored.
|
|
||||||
- Open network ports to access to the Docker container
|
|
||||||
- Give the docker container a name
|
|
||||||
- The container will always restart unless we tell it to stop.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
minecraft-server:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
args:
|
|
||||||
SERVER_MEM: ${SERVER_MEM}
|
|
||||||
SERVER_JAR: ${SERVER_JAR}
|
|
||||||
volumes:
|
|
||||||
- .server-data:/server
|
|
||||||
ports:
|
|
||||||
- 25565:25565 # Game
|
|
||||||
- 25575:25575 # RCON
|
|
||||||
container_name: ${CONTAINER_NAME}
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
server-data:
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Create the _Dockerfile_ file**:
|
|
||||||
|
|
||||||
- requested in docker-compose.yml build stage as context .
|
|
||||||
- The _Dockerfile_ is the spec for software running inside the container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create the file
|
|
||||||
touch Dockerfile
|
|
||||||
# Open the file in a text editor
|
|
||||||
vim Dockerfile
|
|
||||||
```
|
|
||||||
- We use the [eclipse-temurin:21](https://hub.docker.com/_/eclipse-temurin) image to provide the java JRE
|
|
||||||
- Use the required java version for the version of Minecraft you are running
|
|
||||||
- Collect the ARGs from the .env file
|
|
||||||
- Set the environment variables
|
|
||||||
- Set the working directory to /server which on our local system is .server-data
|
|
||||||
- Run the minecraft server jar with the ENV variables
|
|
||||||
|
|
||||||
```Dockerfile
|
|
||||||
FROM eclipse-temurin:21
|
|
||||||
|
|
||||||
ARG SERVER_MEM
|
|
||||||
ARG SERVER_JAR
|
|
||||||
|
|
||||||
ENV SERVER_MEM=${SERVER_MEM}
|
|
||||||
ENV SERVER_JAR=${SERVER_JAR}
|
|
||||||
|
|
||||||
WORKDIR /server
|
|
||||||
|
|
||||||
CMD echo "Minecraft Server is starting\n" && java -Xms128M -Xmx${SERVER_MEM}M -jar ${SERVER_JAR} nogui
|
|
||||||
```
|
|
||||||
|
|
||||||
## **Setup the Server files**
|
|
||||||
|
|
||||||
We’re almost there, just a few steps to go.
|
|
||||||
|
|
||||||
When you try to start the container, you might encounter the following error:
|
|
||||||
```bash
|
|
||||||
Minecraft Server is starting
|
|
||||||
|
|
||||||
Error: Unable to access jarfile server.jar
|
|
||||||
```
|
|
||||||
This happens because we haven't downloaded the server jar file yet. Let's do
|
|
||||||
that now
|
|
||||||
|
|
||||||
1. **Create a folder in the minecraft-server folder called .server-data**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir .server-data
|
|
||||||
cd .server-data
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Download a Minecraft server jar file**:
|
|
||||||
- [Vanilla Minecraft Server](https://www.minecraft.net/en-us/download/server)
|
|
||||||
- Right click the green link that says minecraft_server.<version>.jar and copy the link address
|
|
||||||
- Download with: `curl -o server.jar <link you copied here>`
|
|
||||||
- [Fabric Mod loader server](https://fabricmc.net/use/server/)
|
|
||||||
- Select the Minecraft version, Fabric Loader Version and the Installer Version of your choice. I tend to go with the latest.
|
|
||||||
- Download with: `Copy the curl command under CLI download`
|
|
||||||
|
|
||||||
3. **Go back to the root minecraft-server folder**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ..
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Edit SERVER_JAR in .env with the name of the .jar you just downloaded**:
|
|
||||||
```bash
|
|
||||||
# Open in a text editor
|
|
||||||
vim .env
|
|
||||||
# Enter the following variables
|
|
||||||
CONTAINER_NAME=minecraft-server
|
|
||||||
SERVER_MEM=4096
|
|
||||||
SERVER_JAR=<downloaded-server-file>.jar # Change this to the name of the .jar you downloaded
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Run the container to generate necessary files like 'eula.txt'**:
|
|
||||||
```bash
|
|
||||||
docker compose up
|
|
||||||
```
|
|
||||||
you will see something like this:
|
|
||||||
```bash
|
|
||||||
minecraft-server | Starting net.minecraft.server.Main
|
|
||||||
minecraft-server | [18:05:50] [ServerMain/INFO]: You need to agree to the EULA in order to run the server. Go to eula.txt for more info.
|
|
||||||
minecraft-server exited with code 0
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Accept the EULA**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# edit eula.txt in a text editor
|
|
||||||
vim .server-data/eula.txt
|
|
||||||
# change `eula=false` to `eula=true`
|
|
||||||
eula=true
|
|
||||||
```
|
|
||||||
|
|
||||||
7. **Make any additional changes you want**:
|
|
||||||
- Notable files:
|
|
||||||
- **server.properties** - Configure server name, game mode, world type etc…
|
|
||||||
- **ops.json** - server operators (can be set in-game)
|
|
||||||
- **whitelist.json** - players allowed to join the server (can be set in-game)
|
|
||||||
|
|
||||||
## Run the Minecraft Server
|
|
||||||
|
|
||||||
1. **Run the container in detached mode**:
|
|
||||||
```bash
|
|
||||||
# The -d flag allows the container to run in detached mode, i.e., in the background
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# monitor the logs as the server starts. Use CTRL+C to exit
|
|
||||||
docker logs minecraft-server --follow
|
|
||||||
# If you see the following lines, your server is up and running:
|
|
||||||
[18:17:01] [Server thread/INFO]: Time elapsed: 25529 ms
|
|
||||||
[18:17:01] [Server thread/INFO]: Done (34.571s)! For help, type "help"
|
|
||||||
```
|
|
||||||
|
|
||||||
## **Conclusion**
|
|
||||||
|
|
||||||
Congratulations! You've successfully set up a Minecraft server. Now it's time
|
|
||||||
to play!
|
|
||||||
|
|
||||||
There are many exciting possibilities from here, such as installing mods or
|
|
||||||
creating scripts to manage the server. Enjoy your Minecraft adventures!
|
|
||||||
@@ -39,8 +39,6 @@ a **bare metal server that sucks a lot of power to a low power solution.**
|
|||||||
- 4 x PoE hats
|
- 4 x PoE hats
|
||||||
- 1 x 5 port PoE Switch
|
- 1 x 5 port PoE Switch
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### **Point Of This post**
|
### **Point Of This post**
|
||||||
|
|
||||||
After following this post you will have setup _Docker swarm_ on a cluster of Pis
|
After following this post you will have setup _Docker swarm_ on a cluster of Pis
|
||||||
@@ -329,7 +327,7 @@ vim hosts
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **_[piCluster]_** - specifies a group of hosts
|
- **_[piCluster]_** - specifies a group of hosts
|
||||||
- **_<ip address>_** - change these to the Pis IP addresses
|
- **_`<ip address>`_** - change these to the Pis IP addresses
|
||||||
|
|
||||||
**create the Pi Intercommunication keys**
|
**create the Pi Intercommunication keys**
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
title: Securing SSH 🤖
|
title: Securing SSH 🤖
|
||||||
description: Ever wanted to make your SSH connections more secure. Read this
|
description: Ever wanted to make your SSH connections more secure. Read this
|
||||||
tags:
|
tags:
|
||||||
- HomeLab
|
- homelab
|
||||||
- RemoteAccess
|
- remoteaccess
|
||||||
- SSH
|
- ssh
|
||||||
author: ahmza
|
author: ahmza
|
||||||
date: 2021-12-07
|
date: 2021-12-07
|
||||||
draft: false
|
draft: false
|
||||||
@@ -146,7 +146,7 @@ fine 😊
|
|||||||
- Max Auth Tries 5
|
- Max Auth Tries 5
|
||||||
- PasswordAuthentication no
|
- PasswordAuthentication no
|
||||||
- PermitEmptyPasswords no
|
- PermitEmptyPasswords no
|
||||||
- AllowUsers <user>
|
- AllowUsers `<user>`
|
||||||
- ClientAliveInterval 180
|
- ClientAliveInterval 180
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Setup a HUGO Site
|
|||||||
description: Always wanted to setup a static website. Look no further !! In this post we go over the basics of setting up a HUGO site
|
description: Always wanted to setup a static website. Look no further !! In this post we go over the basics of setting up a HUGO site
|
||||||
tags:
|
tags:
|
||||||
- blog
|
- blog
|
||||||
- HUGO
|
- hugo
|
||||||
- web
|
- web
|
||||||
- development
|
- development
|
||||||
author: ahmza
|
author: ahmza
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ wsl --install
|
|||||||
Go to the Microsoft store and search for a Linux distribution. I prefer
|
Go to the Microsoft store and search for a Linux distribution. I prefer
|
||||||
to use the "Ubuntu" distribution.
|
to use the "Ubuntu" distribution.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Once installed, go ahead and run the distribution, after it finishes setting
|
Once installed, go ahead and run the distribution, after it finishes setting
|
||||||
itself up it will ask you for a username and password. These credentials are
|
itself up it will ask you for a username and password. These credentials are
|
||||||
@@ -51,7 +51,7 @@ I use the windows terminal app to run the Ubuntu distribution, it makes
|
|||||||
customising the environment a lot easier. To download it head over to the
|
customising the environment a lot easier. To download it head over to the
|
||||||
Microsoft store and search for Windows Terminal and download it.
|
Microsoft store and search for Windows Terminal and download it.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
When you login to WSL it will not default to your home directory. To fix that go
|
When you login to WSL it will not default to your home directory. To fix that go
|
||||||
to the Windows Terminal Settings look for Ubuntu or your distributions name and
|
to the Windows Terminal Settings look for Ubuntu or your distributions name and
|
||||||
@@ -61,11 +61,11 @@ change the Starting directory to
|
|||||||
\\wsl$\<distro>\home\<username>
|
\\wsl$\<distro>\home\<username>
|
||||||
```
|
```
|
||||||
|
|
||||||
> Be sure to change "<distro>" to your distributions name
|
> Be sure to change `<distro>` to your distributions name
|
||||||
|
|
||||||
> Be sure to change "<username>" to your username
|
> Be sure to change `<username>` to your username
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## **Conclusion**
|
## **Conclusion**
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Welcome
|
title: Welcome
|
||||||
|
description: The mandatory hello world post — a quick intro to who I am and what this blog is about.
|
||||||
tags:
|
tags:
|
||||||
- blog
|
- blog
|
||||||
- misc
|
- misc
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
# Caddy as a Reverse Proxy
|
||||||
|
|
||||||
|
This document describes how to run [Caddy](https://caddyserver.com/) as a
|
||||||
|
reverse proxy in front of the blog.
|
||||||
|
|
||||||
|
It uses the custom [`caddy-cf`](https://github.com/ahmzaa/caddy-cf) image
|
||||||
|
(`ghcr.io/ahmzaa/caddy-cf:latest`) — a Caddy build with the **Cloudflare DNS
|
||||||
|
module** baked in.
|
||||||
|
|
||||||
|
## Deployment topologies
|
||||||
|
|
||||||
|
There are two supported ways to run Caddy in front of the blog. **Pick one.**
|
||||||
|
|
||||||
|
### 1. Cloudflare in front (current production) — HTTP only
|
||||||
|
|
||||||
|
```
|
||||||
|
Cloudflare (terminates TLS) -> Caddy (HTTP :80) -> SWS (blog:80)
|
||||||
|
```
|
||||||
|
|
||||||
|
Cloudflare proxies the domain and provides the public HTTPS certificate, so
|
||||||
|
Caddy only needs to serve plain **HTTP on :80** and reverse-proxy to the blog.
|
||||||
|
Caddy does **not** provision a TLS cert in this mode. This is the live setup and
|
||||||
|
its config is the committed [`Caddyfile`](../Caddyfile) at the repo root —
|
||||||
|
see [Cloudflare-fronted setup](#cloudflare-fronted-setup-http-only) below.
|
||||||
|
|
||||||
|
Set the Cloudflare **SSL/TLS mode to "Full"** so the Cloudflare→origin hop is
|
||||||
|
still encrypted at the network layer (e.g. over Netbird), even though Caddy's
|
||||||
|
listener is HTTP.
|
||||||
|
|
||||||
|
### 2. Caddy terminates TLS via Cloudflare DNS-01
|
||||||
|
|
||||||
|
```
|
||||||
|
Client -> Caddy (HTTPS :443, ACME DNS-01) -> SWS (blog:80)
|
||||||
|
```
|
||||||
|
|
||||||
|
Caddy obtains/renews the certificate itself using the ACME **DNS-01** challenge
|
||||||
|
through the Cloudflare API — no inbound `80`/`443` from the public internet is
|
||||||
|
required, which suits a Netbird-only setup with no Cloudflare proxy in front.
|
||||||
|
The stock Caddy image cannot do this; the `caddy-cf` build bundles the
|
||||||
|
Cloudflare DNS provider plugin. See the Docker Compose / Kubernetes examples
|
||||||
|
later in this document.
|
||||||
|
|
||||||
|
## Why a reverse proxy?
|
||||||
|
|
||||||
|
The blog is served by [Static Web Server (SWS)](https://static-web-server.net/),
|
||||||
|
a tiny Rust binary that ships static files over plain HTTP on port `80`. SWS is
|
||||||
|
intentionally kept lean — it does **not** terminate TLS, and its compression and
|
||||||
|
security headers are disabled (see `sws.toml`). Those concerns are delegated to
|
||||||
|
the reverse proxy so that each layer owns exactly one job:
|
||||||
|
|
||||||
|
| Concern | Owned by | Notes |
|
||||||
|
| -------------------- | -------- | ------------------------------------------------- |
|
||||||
|
| TLS / HTTPS certs | Cloudflare *or* Caddy | Cloudflare in topology 1; Caddy (DNS-01) in topology 2 |
|
||||||
|
| Compression (gzip/zstd) | Caddy | `encode` directive; SWS compression is off |
|
||||||
|
| Security headers | Caddy | Single source of truth, avoids duplicate headers |
|
||||||
|
| Serving static files | SWS | Reads from `/public`, sets `Cache-Control`/`ETag` |
|
||||||
|
| Cache-Control / ETag | SWS | Declared at the origin, passed through by Caddy |
|
||||||
|
|
||||||
|
### Why the Cloudflare DNS-01 challenge?
|
||||||
|
|
||||||
|
Caddy does **automatic HTTPS** out of the box, but the default HTTP-01 /
|
||||||
|
TLS-ALPN-01 challenges require ports `80`/`443` to be publicly reachable so
|
||||||
|
Let's Encrypt can verify domain ownership. This project's infrastructure is
|
||||||
|
reached over [Netbird](https://netbird.io/) (a private overlay network), so
|
||||||
|
those inbound challenges are not an option.
|
||||||
|
|
||||||
|
The **DNS-01** challenge instead proves domain ownership by creating a TXT
|
||||||
|
record via the Cloudflare API — no inbound connectivity required. The `caddy-cf`
|
||||||
|
image bundles the Cloudflare DNS provider plugin so Caddy can do this. All it
|
||||||
|
needs is a Cloudflare API token with **Zone:DNS:Edit** permission on the
|
||||||
|
`ahmza.com` zone.
|
||||||
|
|
||||||
|
## Origin contract
|
||||||
|
|
||||||
|
Whatever the deployment, the SWS origin exposes:
|
||||||
|
|
||||||
|
- **Protocol:** plain HTTP
|
||||||
|
- **Port:** `80`
|
||||||
|
- **Health endpoint:** `GET /health` → `200 OK`
|
||||||
|
- **Cache headers:** already set by SWS (Caddy should pass them through, not override)
|
||||||
|
|
||||||
|
Caddy only needs to handle TLS (or let Cloudflare do it), add compression +
|
||||||
|
security headers, and forward requests to that origin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cloudflare-fronted setup (HTTP only)
|
||||||
|
|
||||||
|
**This is the current production topology.** The canonical config is the
|
||||||
|
committed [`Caddyfile`](../Caddyfile) at the repo root; copy it to the
|
||||||
|
`caddy_cf` host and reload. Highlights:
|
||||||
|
|
||||||
|
- **`http://blog.ahmza.com`** — the explicit `http://` scheme binds Caddy to
|
||||||
|
`:80` only and stops it from trying to provision a TLS certificate (Cloudflare
|
||||||
|
already serves HTTPS to visitors).
|
||||||
|
- **`reverse_proxy blog:80`** — proxies to the blog (SWS) container by its
|
||||||
|
compose service name. Caddy and the blog container must share a Docker
|
||||||
|
network. Omitting this responder makes Caddy return an empty `NOP` response —
|
||||||
|
i.e. a **blank page**.
|
||||||
|
- **`encode zstd gzip`** — compression happens here (SWS has it disabled).
|
||||||
|
- **`header { ... }`** — security headers (SWS has them disabled).
|
||||||
|
- **`trusted_proxies cloudflare { ... }`** + **`client_ip_headers
|
||||||
|
Cf-Connecting-Ip X-Forwarded-For`** — so the real visitor IP is used for
|
||||||
|
logging/matchers, but only when the request genuinely comes from Cloudflare
|
||||||
|
(prevents header spoofing from any other source). The `cloudflare` IP source
|
||||||
|
([WeidiDeng/caddy-cloudflare-ip](https://github.com/WeidiDeng/caddy-cloudflare-ip),
|
||||||
|
baked into the `caddy-cf` image) auto-fetches the ranges from
|
||||||
|
<https://www.cloudflare.com/ips-v4/> and <https://www.cloudflare.com/ips-v6/>
|
||||||
|
and refreshes them on the configured `interval`, so there is no static list to
|
||||||
|
maintain:
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
trusted_proxies cloudflare {
|
||||||
|
interval 12h
|
||||||
|
timeout 15s
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`client_ip_headers` lists which header holds the real visitor IP:
|
||||||
|
`Cf-Connecting-Ip` is Cloudflare's single authoritative end-user IP, with
|
||||||
|
`X-Forwarded-For` as the fallback. It is only trusted for requests coming from
|
||||||
|
the `trusted_proxies` above.
|
||||||
|
|
||||||
|
> If you ever run without the `caddy-cloudflare-ip` plugin, you can instead
|
||||||
|
> pin the static ranges with `trusted_proxies static <ip/cidr ...>` copied
|
||||||
|
> from the Cloudflare IP pages, but then you must keep them in sync manually.
|
||||||
|
|
||||||
|
Deploy and reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Put the Caddyfile in place on the caddy_cf host (e.g. into the ./conf mount),
|
||||||
|
# then reload without downtime:
|
||||||
|
docker exec caddy_cf caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# Verify from the host (expect HTTP 200, Content-Encoding: gzip, X-Frame-Options):
|
||||||
|
curl -sI -H "Accept-Encoding: gzip" http://localhost/ | grep -iE 'HTTP|content-encoding|x-frame'
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Cloudflare SSL/TLS mode:** set it to **Full** so the Cloudflare→origin hop
|
||||||
|
> is encrypted at the network layer (e.g. over Netbird), even though Caddy's
|
||||||
|
> listener is plain HTTP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The remaining sections describe **topology 2** (Caddy terminates TLS itself via
|
||||||
|
the Cloudflare DNS-01 challenge) for setups without a Cloudflare proxy in front.
|
||||||
|
|
||||||
|
## Docker Compose (Gimli)
|
||||||
|
|
||||||
|
Run Caddy and the blog container on the same Docker network. Caddy reaches the
|
||||||
|
blog by its service name (`blog`) over the internal network, so the blog's port
|
||||||
|
does **not** need to be published to the host.
|
||||||
|
|
||||||
|
The Cloudflare API token is passed to Caddy as the `CF_API_TOKEN` environment
|
||||||
|
variable and referenced from the Caddyfile — keep it out of the committed
|
||||||
|
config. Store it in a `.env` file (git-ignored) next to `compose.yml`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
# .env
|
||||||
|
CF_API_TOKEN=your-cloudflare-api-token-with-zone-dns-edit
|
||||||
|
```
|
||||||
|
|
||||||
|
### `compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
blog:
|
||||||
|
image: ghcr.io/ahmzaa/blog-ahmza-com
|
||||||
|
restart: unless-stopped
|
||||||
|
# No host port mapping needed — only Caddy talks to it, over the
|
||||||
|
# internal Docker network. Remove the previous "80:80" publish.
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: ghcr.io/ahmzaa/caddy-cf:latest # Caddy + Cloudflare DNS module
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: caddy-cf
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp" # HTTP/3 (QUIC)
|
||||||
|
environment:
|
||||||
|
# Consumed by the {env.CF_API_TOKEN} placeholder in the Caddyfile.
|
||||||
|
- CF_API_TOKEN=${CF_API_TOKEN}
|
||||||
|
volumes:
|
||||||
|
- ./conf:/etc/caddy # contains the Caddyfile
|
||||||
|
- ./data:/data # persists certificates — DO NOT lose this
|
||||||
|
- ./config:/config
|
||||||
|
```
|
||||||
|
|
||||||
|
Place the Caddyfile at `./conf/Caddyfile`.
|
||||||
|
|
||||||
|
### `./conf/Caddyfile`
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
# Reusable Cloudflare DNS-01 TLS snippet. The token is read from the
|
||||||
|
# CF_API_TOKEN environment variable so it never lives in the config file.
|
||||||
|
(cloudflare) {
|
||||||
|
tls {
|
||||||
|
dns cloudflare {env.CF_API_TOKEN}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blog.ahmza.com {
|
||||||
|
# Obtain/renew the cert via the Cloudflare DNS-01 challenge.
|
||||||
|
import cloudflare
|
||||||
|
|
||||||
|
# Reverse proxy to the blog container by its compose service name.
|
||||||
|
reverse_proxy blog:80 {
|
||||||
|
# Use the SWS health endpoint for active upstream checks.
|
||||||
|
health_uri /health
|
||||||
|
health_interval 10s
|
||||||
|
health_timeout 2s
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compression is done here, not in SWS.
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Security headers live here so there is a single source of truth.
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
# Remove Caddy's default Server header
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Certificate persistence:** the `./data` volume stores the issued
|
||||||
|
> certificates and ACME account key. Keep it — deleting it forces Caddy to
|
||||||
|
> re-request certificates and can hit Let's Encrypt rate limits.
|
||||||
|
|
||||||
|
Bring it up with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kubernetes (Raspberry Pi cluster)
|
||||||
|
|
||||||
|
The blog runs as a `Deployment` behind a `Service`. Caddy runs as its own
|
||||||
|
`Deployment` and proxies to that Service via in-cluster DNS
|
||||||
|
(`blog.blog-ahmza-com.svc.cluster.local`, or just `blog` from within the same
|
||||||
|
namespace).
|
||||||
|
|
||||||
|
### Cloudflare API token Secret
|
||||||
|
|
||||||
|
Store the Cloudflare API token in a `Secret` and inject it into the Caddy
|
||||||
|
container as the `CF_API_TOKEN` environment variable (never bake it into the
|
||||||
|
ConfigMap):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic caddy-cloudflare \
|
||||||
|
--namespace blog-ahmza-com \
|
||||||
|
--from-literal=CF_API_TOKEN='your-cloudflare-api-token-with-zone-dns-edit'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddyfile via ConfigMap
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: caddy-config
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
data:
|
||||||
|
Caddyfile: |
|
||||||
|
(cloudflare) {
|
||||||
|
tls {
|
||||||
|
dns cloudflare {env.CF_API_TOKEN}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blog.ahmza.com {
|
||||||
|
import cloudflare
|
||||||
|
|
||||||
|
reverse_proxy blog:80 {
|
||||||
|
health_uri /health
|
||||||
|
health_interval 10s
|
||||||
|
health_timeout 2s
|
||||||
|
}
|
||||||
|
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddy Deployment + Service
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: caddy
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: caddy
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: caddy
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: caddy
|
||||||
|
image: ghcr.io/ahmzaa/caddy-cf:latest # Caddy + Cloudflare DNS module
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
- containerPort: 443
|
||||||
|
- containerPort: 443
|
||||||
|
protocol: UDP
|
||||||
|
env:
|
||||||
|
- name: CF_API_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: caddy-cloudflare
|
||||||
|
key: CF_API_TOKEN
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/caddy
|
||||||
|
- name: data
|
||||||
|
mountPath: /data # certificate storage — must persist
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: caddy-config
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: caddy-data
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: caddy-data
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 256Mi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: caddy
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer # or NodePort, depending on the cluster
|
||||||
|
selector:
|
||||||
|
app: caddy
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
targetPort: 443
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Blog Service:** the existing `blog` Service should expose port `80` and
|
||||||
|
> target the SWS container port `80`. Caddy reaches it as `blog:80` from inside
|
||||||
|
> the same namespace.
|
||||||
|
|
||||||
|
> **Certificate persistence:** the `caddy-data` PVC is the K8s equivalent of the
|
||||||
|
> `./data` Docker volume. Without a persistent volume, a pod restart wipes
|
||||||
|
> the certificates and re-triggers ACME issuance.
|
||||||
|
|
||||||
|
> **DNS-01 and Netbird:** because certificates are issued via the Cloudflare
|
||||||
|
> DNS-01 challenge, the cluster does **not** need inbound `80`/`443` from the
|
||||||
|
> public internet for ACME. The `LoadBalancer`/`NodePort` above only needs to be
|
||||||
|
> reachable by your clients (e.g. over Netbird or your LAN).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes and gotchas
|
||||||
|
|
||||||
|
- **DNS-01 needs no inbound reachability.** Because the `caddy-cf` image solves
|
||||||
|
the ACME challenge via the Cloudflare API (DNS-01), Caddy does **not** need
|
||||||
|
ports `80`/`443` exposed to the public internet to issue certificates. This is
|
||||||
|
what makes it work behind Netbird. It does need outbound HTTPS to the Let's
|
||||||
|
Encrypt and Cloudflare APIs.
|
||||||
|
- **Cloudflare API token scope.** The token needs **Zone → DNS → Edit** on the
|
||||||
|
`ahmza.com` zone. Keep it in a `.env` file (Docker) or a `Secret` (K8s), never
|
||||||
|
in the committed Caddyfile — the config references it as `{env.CF_API_TOKEN}`.
|
||||||
|
- **DNS must be managed by Cloudflare.** The `ahmza.com` zone has to be hosted on
|
||||||
|
Cloudflare for the DNS-01 challenge to create the required `_acme-challenge`
|
||||||
|
TXT record.
|
||||||
|
- **Do not double-compress.** SWS compression is off by design; Caddy's `encode`
|
||||||
|
directive is the only place compression happens. Enabling both wastes CPU.
|
||||||
|
- **Do not duplicate headers.** Security headers are set in the `Caddyfile`. SWS
|
||||||
|
has `security-headers = false` so there is no conflict.
|
||||||
|
- **Cache-Control comes from the origin.** SWS already sets long-lived caching on
|
||||||
|
static assets and short/no-cache on HTML. Caddy passes these through; avoid
|
||||||
|
adding conflicting `Cache-Control` header directives in the `Caddyfile`.
|
||||||
|
- **Health checks.** Both examples use SWS's `/health` endpoint for active
|
||||||
|
upstream health checking, and it also works as a K8s liveness/readiness probe
|
||||||
|
on the blog pod itself.
|
||||||
|
- **HTTP/3.** The Docker example publishes `443/udp` to enable QUIC. Remove it if
|
||||||
|
your network path does not support UDP.
|
||||||
@@ -1 +1 @@
|
|||||||
<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix .Destination "http" }} target="_blank"{{ end }}>{{ .Text | safeHTML }}</a>
|
<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix .Destination "http" }} target="_blank" rel="noopener noreferrer"{{ end }}>{{ .Text | safeHTML }}</a>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<br>
|
<br>
|
||||||
<div class="flex items-center gap-x-5">
|
<div class="flex items-center gap-x-5">
|
||||||
<a class="text-sm text-neutral-500 dark:text-neutral-400 rel='me'" href="https://mastodon.social/@ahmza">Mastodon</a>
|
<a class="text-sm text-neutral-500 dark:text-neutral-400" rel="me noopener noreferrer" target="_blank" href="https://mastodon.social/@ahmza">Mastodon</a>
|
||||||
<a class="text-sm text-neutral-500 dark:text-neutral-400 rel='me'" href="https://github.com/ahmzaa">Github</a>
|
<a class="text-sm text-neutral-500 dark:text-neutral-400" rel="me noopener noreferrer" target="_blank" href="https://github.com/ahmzaa">Github</a>
|
||||||
<a class="text-sm text-neutral-500 dark:text-neutral-400 rel='me'" href="https://www.linkedin.com/in/ahmza/">Linkedin</a>
|
<a class="text-sm text-neutral-500 dark:text-neutral-400" rel="me noopener noreferrer" target="_blank" href="https://www.linkedin.com/in/ahmza/">Linkedin</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<img src="{{ .Get "url"}}" alt="{{ .Get "alt"}}" style="display:block;margin-left:auto;margin-right:auto;width:50%;height:auto;padding:10px;">
|
<img src="{{ .Get "url"}}" alt="{{ .Get "alt"}}" loading="lazy" decoding="async" style="display:block;margin-left:auto;margin-right:auto;width:50%;height:auto;padding:10px;">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<video autoplay loop muted playsinline aria-label='{{ .Get "label"}}' style="display:block;margin-left:auto;margin-right:auto;width:50%;height:auto;padding:10px;">
|
<video autoplay loop muted playsinline preload="metadata" aria-label='{{ .Get "label"}}' style="display:block;margin-left:auto;margin-right:auto;width:50%;height:auto;padding:10px;">
|
||||||
{{ with .Get "mp4" }}<source src="{{ . }}" type="video/mp4">{{ end }}
|
{{ with .Get "mp4" }}<source src="{{ . }}" type="video/mp4">{{ end }}
|
||||||
{{ with .Get "webm" }}<source src="{{ . }}" type="video/webm">{{ end }}
|
{{ with .Get "webm" }}<source src="{{ . }}" type="video/webm">{{ end }}
|
||||||
</video>
|
</video>
|
||||||
|
|||||||
-70
@@ -1,70 +0,0 @@
|
|||||||
# auto detects a good number of processes to run
|
|
||||||
worker_processes auto;
|
|
||||||
|
|
||||||
#Provides the configuration file context in which the directives that affect connection processing are specified.
|
|
||||||
events {
|
|
||||||
# Sets the maximum number of simultaneous connections that can be opened by a worker process.
|
|
||||||
worker_connections 1024;
|
|
||||||
# Tells the worker to accept multiple connections at a time
|
|
||||||
multi_accept on;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
http {
|
|
||||||
# what times to include
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
# what is the default one
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
# Sets the path, format, and configuration for a buffered log write
|
|
||||||
log_format compression '$remote_addr - $remote_user [$time_local] '
|
|
||||||
'"$request" $status $upstream_addr '
|
|
||||||
'"$http_referer" "$http_user_agent"';
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 9;
|
|
||||||
gzip_buffers 10 8k;
|
|
||||||
gzip_http_version 1.1;
|
|
||||||
gzip_types text/plain text/css text/xml text/javascript image/webp image/svg+xml image/x-icon application/javascript application/x-javascript application/json application/xml application/xml+rss;
|
|
||||||
|
|
||||||
server {
|
|
||||||
# listen on port 80
|
|
||||||
listen 80;
|
|
||||||
# save logs here
|
|
||||||
access_log /var/log/nginx/access.log compression;
|
|
||||||
|
|
||||||
# where's the root dir
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
# what file to serve as index
|
|
||||||
index index.html index.htm;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
# First attempt to serve request as file, then
|
|
||||||
# as directory, then fall back to redirecting to index.html
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Media: images, icons, video, audio, HTC
|
|
||||||
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|webp)$ {
|
|
||||||
expires 1y;
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Javascript and CSS files
|
|
||||||
location ~* \.(?:css|js)$ {
|
|
||||||
try_files $uri =404;
|
|
||||||
expires 1y;
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Any route containing a file extension (e.g. /devicesfile.js)
|
|
||||||
location ~ ^.+\..+$ {
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,7 @@ media_folder: "assets/uploaded/img"
|
|||||||
public_folder: "/uploaded/img"
|
public_folder: "/uploaded/img"
|
||||||
|
|
||||||
## SiteTitle
|
## SiteTitle
|
||||||
site_url: htpps://blog.ahmza.com
|
site_url: https://blog.ahmza.com
|
||||||
display_url: blog.ahmza.com
|
display_url: blog.ahmza.com
|
||||||
logo_url: https://blog.ahmza.com/logo.webp
|
logo_url: https://blog.ahmza.com/logo.webp
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,13 @@
|
|||||||
<!-- Include the script that enables Netlify Identity on this page. -->
|
<!-- Include the script that enables Netlify Identity on this page. -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Include the script that builds the page and powers Decap CMS -->
|
<!-- Sveltia CMS, pinned to a specific version with Subresource Integrity.
|
||||||
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
|
Bump the version + regenerate the integrity hash when upgrading:
|
||||||
|
curl -sS https://cdn.jsdelivr.net/npm/@sveltia/cms@<ver>/dist/sveltia-cms.js \
|
||||||
|
| openssl dgst -sha384 -binary | openssl base64 -A -->
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/@sveltia/cms@0.172.4/dist/sveltia-cms.js"
|
||||||
|
integrity="sha384-Sj4Mfbg9OjjwG2ZE/YeUYu7xbZRTXGFg/wa/nszj3KzItKuRLEmGqSpV5C9YI2Ge"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,47 @@
|
|||||||
|
[general]
|
||||||
|
|
||||||
|
#### Server
|
||||||
|
|
||||||
|
# Listen on all interfaces, port 80 (matches previous nginx setup)
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 80
|
||||||
|
|
||||||
|
# Root directory of the static site (Hugo `public/` output)
|
||||||
|
root = "/public"
|
||||||
|
|
||||||
|
# Number of worker threads (0 = auto-detect based on CPU cores)
|
||||||
|
threads-multiplier = 1
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
log-level = "info"
|
||||||
|
|
||||||
|
#### Performance / caching
|
||||||
|
|
||||||
|
# Compression is handled by the upstream reverse proxy, so it is disabled
|
||||||
|
# here to avoid doing the work twice (wasted CPU per request). Re-enable if
|
||||||
|
# SWS is ever exposed directly without a compressing proxy in front.
|
||||||
|
compression = false
|
||||||
|
compression-static = false
|
||||||
|
|
||||||
|
# Emit Cache-Control + ETag headers with sensible per-content-type defaults.
|
||||||
|
# The origin is the correct place to declare cacheability; the proxy/CDN
|
||||||
|
# passes these through. SWS applies long-lived caching to static assets
|
||||||
|
# (images, css, js, fonts) and short/no-cache to HTML.
|
||||||
|
cache-control-headers = true
|
||||||
|
|
||||||
|
#### Routing
|
||||||
|
|
||||||
|
# No SPA-style page fallback: this is a static Hugo site, not a single-page app.
|
||||||
|
# Missing paths should return Hugo's generated 404.html with a real 404 status
|
||||||
|
# (SWS serves /public/404.html automatically) rather than masking broken links
|
||||||
|
# with a 200 homepage response, which harms SEO (soft 404s).
|
||||||
|
|
||||||
|
#### Security
|
||||||
|
|
||||||
|
# Security headers are set by the upstream reverse proxy, so they are
|
||||||
|
# disabled here to avoid duplicate/conflicting headers. Let one layer own
|
||||||
|
# the security posture. Re-enable if SWS is exposed without such a proxy.
|
||||||
|
security-headers = false
|
||||||
|
|
||||||
|
# Health endpoint at /health for container/K8s liveness probes
|
||||||
|
health = true
|
||||||
Reference in New Issue
Block a user