Setup my second brain

Table of Contents

Diagram

my_second_brain

Prerequisites

  • A first brain :)
  • A server (I personally use a VPS but it can be whatever server)

Application list

  • Obsidian (best notes application ever)
  • Syncthing (best synchronization application ever)
  • Nginx (Web server)
  • Hugo (Create an HTML website from Markdown files)
  • n8n (great automation tool that connect tons of apps together)
  • Other tools:
    • Git
    • GitLab or GitHub
    • Go (needed by Hugo)
    • inotify-tools
    • A custom script for automation

Depending on your needs, not all of these tools are necessarily required.
I’ve personally implemented all of them, but if you’re simply looking for a note-taking tool that syncs everywhere, you can just stick with Obsidian and Syncthing!

Setup the server

Set timezone

# Check actual timezeone:
timedatectl
# If necessary, modify it:
timedatectl set-timezone <timezone>

Fill in the <timezeone> field
Example: Europe/Paris

Update repos and apps

apt update && apt upgrade

Add user and switch to it

adduser <user>
usermod -aG sudo <user>
su - <user>

Fill in the <user> field

Install applications

Obsidian

Obsidian should be already installed on one or more personal devices (laptop, smartphone, etc.).

  • ⚠️Important settings in Obsidian⚠️

Settings > Files and links

New link format: Relative path to file

  • Shortest path when possible is a problem in Obsidian when some files have identical names.
  • Absolute path in vault is a problem when Hugo create a link to another webpage because md file is targeted instead of html.
  • Relative path to file is the best option but need to modify Hugo generated links (managed later in Automation).

Use [[Wikilinks]]: No

  • To be compatible with standard Markdown format and be able to display images in GitLab.

Default location for new attachments: In subfolder under current folder

  • That allows attached files to be stored in the same directory as the content, rather than having them all mixed at the root.

Subfolder name: <attachment_folder>

Fill in the <attachment_folder> field
Example: _attachments

  • Keep in mind that all attached files will be stored in a subfolder named as this.

Git

  • Install Git on the server:
apt install git-all
git --version

Set the public key on remote Git

On the server:

  • Create an SSH key if there isn’t:
# Check if an SSH key exist already
ls ~/.ssh/id_rsa.pub
# If not, create it
ssh-keygen -t rsa -b 4096 -C "<email>"
# Copy server public key
cat ~/.ssh/id_rsa.pub

Fill in the <email> field

On remote Git:

  • Create a remote repository.

  • Add the public key:
    For example, in GitLab:

    • Profile > Preferences > SSH Keys > Add new key
    • Paste public key and give a title
    • Set Expiration date if necessary
  • Test access from the server:

ssh -T git@<remote_git>

Fill in the <remote_git> field
Example: gitlab.com

Initialize repository

  • Allow force push on remote Git repository:
    For example, in GitLab:
    Settings > Repository Settings

On the server:

  • Initialize a git repository:
# Open Obsidian directory:
cd /path/to/Obsidian/files
# Initialize a git repository:
git init
# Set Git username and email globally:
git config --global user.name "<user>"
git config --global user.email "<email>"
# Add remote Git repository:
git remote add origin git@<remote_git>:<user>/<repo_name>.git

Fill in the <user>, <email>, <remote_git> and <repo_name> fields

  • Add some patterns to ignore in a .gitignore file:
vi .gitignore

.gitignore:

/.git
/.obsidian
*.swp
*.tmp
  • Push Obsidian content to remote Git:
git add .
git commit -m "Initial commit from server"
git push -u origin main --force

Syncthing

  • Install Syncthing on the server:
# Add the release PGP keys:
sudo mkdir -p /etc/apt/keyrings
sudo curl -L -o /etc/apt/keyrings/syncthing-archive-keyring.gpg https://syncthing.net/release-key.gpg

# Add the "stable" channel to your APT sources:
echo "deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg] https://apt.syncthing.net/ syncthing stable" | sudo tee /etc/apt/sources.list.d/syncthing.list

# Install Syncthing
sudo apt-get update && sudo apt install syncthing
  • Edit configuration and replace 127.0.0.1 by 0.0.0.0 to listen on every network interface:
vi /home/<user>/.local/state/syncthing/config.xml

Fill in the <user> field

  • Install service to start Syncthing automatically:
sudo vi /etc/systemd/system/syncthing@.service

syncthing@.service:

[Unit]
Description=Syncthing - Open Source Continuous File Synchronization for %I
Documentation=man:syncthing(1)
After=network.target
StartLimitIntervalSec=60
StartLimitBurst=4

[Service]
User=%i
ExecStart=/usr/bin/syncthing serve --no-browser --no-restart --logflags=0
Restart=on-failure
RestartSec=1
SuccessExitStatus=3 4
RestartForceExitStatus=3 4

# Hardening
ProtectSystem=full
PrivateTmp=true
SystemCallArchitectures=native
MemoryDenyWriteExecute=true
NoNewPrivileges=true

# Elevated permissions to sync ownership (disabled by default),
# see https://docs.syncthing.net/advanced/folder-sync-ownership
#AmbientCapabilities=CAP_CHOWN CAP_FOWNER

[Install]
WantedBy=multi-user.target
  • Enable and restart service:
sudo systemctl enable syncthing@<user>.service
sudo systemctl restart syncthing@<user>.service

Fill in the <user> field

  • Open Web interface at http://<ip_address>:8384

Fill in the <ip_address> field

  • Configure Syncthing:
    • Set a user and password (72 characters maximum) to secure the GUI
    • Set a device name
    • Add a folder to sync:
      • Folder Label (Example: Obsidian)
      • Folder path: /path/to/Obsidian/files
      • [optional] Set ignore patterns:
//Better if patterns that are the same on both sides are ignored
/.obsidian
/.git
/.stfolder
/.gitignore

On other device(s):

  • Install Syncthing
  • Add server remote device
  • Share synced folder from server to other device(s)

Nginx

  • Install Nginx on the server:
sudo apt update && sudo apt install nginx
sudo systemctl status nginx
  • Define and enable a new website:
touch /etc/nginx/sites-available/<website_name>
ln -s /etc/nginx/sites-available/<website_name> /etc/nginx/sites-enabled/
vi /etc/nginx/sites-available/<website_name>

Fill in the <website_name> field
Example: my_second_brain

<website_name>:

server {
    listen 80;
    server_name <ip_address>;
    root /var/www/<website_name>/public;
    index index.html;
    location / {
        try_files $uri $uri/ =404;
    }
}

Fill in the <ip_address> and <website_name> fields

  • Test the configuration and restart service:
nginx -t
systemctl restart nginx

HTTPS setup

  • Install Certbot:
sudo apt update
sudo apt install certbot python3-certbot-nginx
  • Check actual Nginx configuration:
cat /etc/nginx/sites-available/brain2
  • Setup HTTPS:
sudo certbot --nginx
  • Cerbot will:

    • Detect Nginx configuraion
    • Configure HTTPS
    • Get a Let’s Encrypt certificate
    • Redirect HTTP to HTTPS
  • stdout:

Requesting a certificate for julianhub.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/julianhub.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/julianhub.com/privkey.pem
This certificate expires on 2025-07-30.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for julianhub.com to /etc/nginx/sites-enabled/brain2
Congratulations! You have successfully enabled HTTPS on https://julianhub.com
  • Check new Nginx configuration:
cat /etc/nginx/sites-available/brain2
  • Test automatic renew:
sudo certbot renew --dry-run
  • Restart Nginx:
sudo systemctl reload nginx

Go

  • Install on the server:
cd /var/tmp
GOFILE=<go_tar.gz_binary>
wget https://go.dev/dl/$GOFILE
# Install:
rm -rf /usr/local/go && tar -C /usr/local -xzf $GOFILE
# Add /usr/local/go/bin to the `PATH` environment variable
export PATH=$PATH:/usr/local/go/bin
go version
rm -rf /var/tmp/$GOFILE

Fill in the <go_tar.gz_binary> field
Example: go1.24.2.linux-amd64.tar.gz

Hugo

  • Install on the server:
sudo apt update && sudo apt install hugo
  • Create a new website:
cd /var/www
hugo new site <website_name>
cd /var/www/<website_name>

Fill in the <website_name> field

  • Choose and install a theme:
# Initialize a Git repository to install themes:
git init
# Choose a theme at https://themes.gohugo.io/themes
git submodule add <theme_git_url> themes/<theme_name>
# Copy and paste the config example of theme page and paste it in Hugo configuration:
cp -p hugo.toml hugo.toml.org
>hugo.toml && vi hugo.toml
# If the theme contain [module], remove the entire block

Fill in the <theme_git_url> and <theme_name> fields
Example: git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke

  • Add this block at the end of hugo.toml:
[module]
  [[module.mounts]]
    source = "/path/to/Obsidian/subfolder/dedicated/to/website/"
    # Example:
    #source = "/path/to/Obsidian/blog/"
    target = "content/posts"
  • Generate the website:
# Test website:
hugo server -t terminal --bind 0.0.0.0 --baseURL http://<ip_address>:1313/
# Open http://<ip_address>:1313

# If this is OK, generate the website:
hugo --cleanDestinationDir

Fill in the <ip_address> field

  • Add a layout to open external links in a new tab:
mkdir -p /var/www/<website_name>/layouts/_default/_markup
vi /var/www/<website_name>/layouts/_default/_markup/render-link.html

Fill in the <website_name> field

render-link.html:

{{- $link := .Destination -}}
<a href="{{ $link }}" 
   {{ if strings.HasPrefix $link "http" }}target="_blank" rel="noopener noreferrer"{{ end }}>
   {{ .Text | safeHTML }}
</a>

n8n

I invite you to follow the documentation which is very good because depending on your personal setup, the installation can be very different (local or remote, docker or not, with or without domain name, etc.).

Traefik uses port 443 by default so you will need to replace them with other ports in the docker-compose.yml because it’s already used by Nginx for HTTPS.
Exemple:

    ports:
      - "8080:8080"
      - "8443:443"

FYI, it is possible to install n8n even without a domain name. You just don’t have to use Traefik. Ask me for the configuration if needed.

Automation

Now comes the cool part: automation! :)

Obsidian ⇒ Remote Git + website

To do this, you first need to install inotify-toolsthat will be used in a script to detect any modification and then execute some actions.

Well, in fact there are two scripts: the first is a Bash script and the second one is a Python script called by the first one.

Why two scripts ? Because first actions are quite simple and can be done in a basic Bash script while the last action use regular expression so it’s really easier to do in Python.
That being said, I may merge the two one day for simplicity…

These two scripts execute 3 main actions:

  • Run Bash script:
    • (1) Update remote repository
    • (2) Update website
    • Run Python script:
      • (3) Manage links

inotify-tools

  • Install inotify-tools:
sudo apt-get update && sudo apt install inotify-tools

Scripts

  • Create the Bash automation script and an the associated service:
vi ~/auto_push_notes.bash

auto_push_notes.bash

chmod +x ~/auto_push_notes.bash
vi /etc/systemd/system/auto_push_notes.service

auto_push_notes.service

systemctl daemon-reload
systemctl enable auto_push_notes.service
systemctl start auto_push_notes.service
systemctl status auto_push_notes.service
  • Create the Python script:
vi ~/mod_notes_links.py

mod_notes_links.py

Enable automation

  • Activate the service:
systemctl daemon-reload
systemctl enable auto_push_notes.service
systemctl start auto_push_notes.service
systemctl status auto_push_notes.service

Remote Git ⇒ Obsidian

This part can be done with a very simple workflow in n8n (3 block steps).

In remote Git:

  • Add a webhook
    • URL: http://<domain_or_ip_address>:<port>/webhook-test/git-webhook

      Fill in the <domain_or_ip_address> and <port> fields

    • Push events: enable
    • Define a custom secret token (like you define a password or whatever)

This webhook will fire as soon as a change is made to a file and will trigger the n8n workflow that we will create now:

In n8n:

  • Create a new workflow and name it as you want

  • Add a “Webhook” node

    • URL: paste the URL defined in remote Git webhook
    • HTTP Method: POST
    • Path: /git-webhook
  • Add an “If” node

    • Value 1: {{$json["headers"]["x-git-token"]}}
    • Operation: Equal
    • Value 2: <secret_token>

Fill in the <secret_token> field

  • Add an “Execute Command” node
    • Set the credential
    • Command: git pull
    • Working directory: /path/to/Obsidian/files

And that’s it! :)