The hosts took a deserved lead before 15 minutes when the ball fell kindly to Rodrygo just outside the penalty area and he shot past Shakhtar keeper Anatolii Trubin to stake Madrid to a 1-0 advantage.
The LaLiga holders looked to be well on their way to a straightforward win when Vincius Junior doubled their lead from a Rodrygo assist in the 28th minute, but Shakhtar hit back before the half through Oleksandr Zubkov to make it 2-1 at the break.
Madrid controlled possession for much of the second half, but the visiting Ukrainian side were dangerous situationally through set pieces and on the counter-attack forcing Carlo Ancelotti's side to battle to the end for their three points.
The result keeps Madrid atop Group F on nine points with three wins from three matches, while Shakhtar are in second well off the leaders' pace at four points.
"It was a game that we should have won 7-1," Real midfielder Toni Kroos told Movistar Plus. "It's hard to believe that we won by just 2-1 given the chances that we had.
"We needed to keep our focus until the end because it was only one goal deficit and any mistake would be costly."
Rodrygo gave Real Madrid the lead in the 13th minute with an angled strike from the edge of the box.
He received a pass from Vinicius and sprinted towards the goal between three defenders before lashing a precise shot to the goalkeeper's left.
Rodrygo has scored 11 goals for Real in the Champions League, equalling club hero Raul Gonzalez for the most goals scored for the team in the competition while aged 21 or under.
"[Rodrygo] can progress, he has more confidence, he's more decisive, he's progressing well but he's still young," Ancelotti said of the in-form Brazil international. "He deserves it with what he does in every game."
Vinicius extended Real's lead 15 minutes later, finishing a fine collective effort by his team mates with a close-range strike after beating the offside trap.
Vinicius missed two clear chances in the following minutes and left Shakhtar in a position to close the gap with a beautiful goal by Zubkov.
The Ukrainian striker reduced the deficit following a counter-attack six minutes before the break with a magnificent acrobatic volley from a Bohdan Mykhailichenko cross.
"We dropped our intensity, thinking the game was controlled and comfortable," Ancelotti said. "I had a word at halftime because the game wasn't over. It's quite normal, it can happen. We were playing, it seemed easy, but those are dangerous games, you go ahead, but you can concede. At the back we only gave up one chance on the counter-attack.
"This team relaxes when it can relax, when it can't, it doesn't. It's normal when you're 2-0 up and the game controlled, you can run less, you think it's simple. It can happen, they're people. But I'm happy, the team played well, the three in attack combined well. Valverde got forward. Kroos and [Aurelien] Tchouameni controlled the ball well. I'm not worried, but of course intensity is important."
Shakhtar's Trubin was the star of the second half with a series of brilliant saves to stop Real extending their lead.
With Real pressing Trubin's box relentlessly, they were exposed to counter-attacks and Mykhailo Mudryk almost tied the match in one of those, in the 51st minute, but his effort was denied one-on-one by his countryman Andriy Lunin, who was standing in for the injured Thibaut Courtois.
Vinicius, Benzema, Rodrygo and Valverde all had several chances denied by Trubin later.
Substitute Sergio Asensio smashed a thundering strike off the far post in added time.
Shakhtar and Real Madrid will face each other again next week in Warsaw where Shakhtar are hosting their Champions League matches due to the situation in Ukraine.